diff --git a/.eslintrc.js b/.eslintrc.js index c3d03a4f68..e44e804d89 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -36,6 +36,15 @@ module.exports = { // Required checks 'indent': ['error', 2], + 'keyword-spacing': ['error'], + 'max-len': [ + 'error', + { + 'code': 120, + 'ignoreUrls': true + } + ], + 'object-curly-spacing': [2, 'always'], '@typescript-eslint/explicit-function-return-type': [ 'error', { @@ -44,5 +53,8 @@ module.exports = { '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}] } }; diff --git a/.github/actions/send-email/README.md b/.github/actions/send-email/README.md new file mode 100644 index 0000000000..ab3f93a154 --- /dev/null +++ b/.github/actions/send-email/README.md @@ -0,0 +1,59 @@ +# Send Email GitHub Action + +This is a minimalistic GitHub Action for sending emails using the Mailgun API. +Specify the Mailgun API key along with the Mailgun domain and message to +be sent. + +## Inputs + +### `api-key` + +**Required** Mailgun API key. + +### `domain` + +**Required** Mailgun domain name. + +### `from` + +**Required** Sender's email address. Ex: 'Hello User ' (defaults to 'user@{domain}`). + +### `to` + +**Required** Recipient's email address. You can use commas to separate multiple recipients. + +### `cc` + +Email addresses to Cc. + +### `subject` + +**Required** Message subject. + +### `text` + +Text body of the message. + +### `html` + +HTML body of the message. + +## Example usage + +``` +- name: Send Email + uses: firebase/firebase-admin-node/.github/actions/send-email + with: + api-key: ${{ secrets.MAILGUN_API_KEY }} + domain: ${{ secrets.MAILGUN_DOMAIN }} + from: 'User ' + html: '

Testing some Mailgun awesomness!

' + to: 'foo@example.com' +``` + +## Implementation + +This Action uses the `mailgun.js` NPM package to send Emails. + +When making a code change remember to run `npm run pack` to rebuild the +`dist/index.js` file which is the executable of this Action. diff --git a/.github/actions/send-email/action.yml b/.github/actions/send-email/action.yml new file mode 100644 index 0000000000..65956f9a12 --- /dev/null +++ b/.github/actions/send-email/action.yml @@ -0,0 +1,44 @@ +# Copyright 2021 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: 'Send email Action' +description: 'Send emails using Mailgun from GitHub Actions workflows.' +inputs: + api-key: + description: Mailgun API key. + required: true + domain: + description: Mailgun domain name. + required: true + from: + description: Senders name and email address. + required: true + to: + description: Recipient's email address. You can use commas to separate multiple recipients. + required: true + cc: + description: Email addresses to Cc. + required: false + subject: + description: Message subject. + required: true + text: + description: Text body of the message. + required: false + html: + description: HTML body of the message. + required: false +runs: + using: 'node12' + main: 'dist/index.js' diff --git a/.github/actions/send-email/dist/index.js b/.github/actions/send-email/dist/index.js new file mode 100644 index 0000000000..d1817b6db8 --- /dev/null +++ b/.github/actions/send-email/dist/index.js @@ -0,0 +1,2105 @@ +module.exports = +/******/ (function(modules, runtime) { // webpackBootstrap +/******/ "use strict"; +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ __webpack_require__.ab = __dirname + "/"; +/******/ +/******/ // the startup function +/******/ function startup() { +/******/ // Load entry module and return exports +/******/ return __webpack_require__(104); +/******/ }; +/******/ +/******/ // run startup +/******/ return startup(); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ 2: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var abort = __webpack_require__(762) + , async = __webpack_require__(792) + ; + +// API +module.exports = terminator; + +/** + * Terminates jobs in the attached state context + * + * @this AsyncKitState# + * @param {function} callback - final callback to invoke after termination + */ +function terminator(callback) +{ + if (!Object.keys(this.jobs).length) + { + return; + } + + // fast forward iteration index + this.index = this.size; + + // abort jobs + abort(this); + + // send back results we have so far + async(callback)(null, this.results); +} + + +/***/ }), + +/***/ 70: +/***/ (function(module) { + +// populates missing values +module.exports = function(dst, src) { + + Object.keys(src).forEach(function(prop) + { + dst[prop] = dst[prop] || src[prop]; + }); + + return dst; +}; + + +/***/ }), + +/***/ 82: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Sanitizes an input into a string so it can be passed into issueCommand safely + * @param input input to sanitize into a string + */ +function toCommandValue(input) { + if (input === null || input === undefined) { + return ''; + } + else if (typeof input === 'string' || input instanceof String) { + return input; + } + return JSON.stringify(input); +} +exports.toCommandValue = toCommandValue; +//# sourceMappingURL=utils.js.map + +/***/ }), + +/***/ 87: +/***/ (function(module) { + +module.exports = require("os"); + +/***/ }), + +/***/ 89: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(258) + , initState = __webpack_require__(125) + , terminator = __webpack_require__(2) + ; + +// Public API +module.exports = parallel; + +/** + * Runs iterator over provided array elements in parallel + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function parallel(list, iterator, callback) +{ + var state = initState(list); + + while (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, function(error, result) + { + if (error) + { + callback(error, result); + return; + } + + // looks like it's the last one + if (Object.keys(state.jobs).length === 0) + { + callback(null, state.results); + return; + } + }); + + state.index++; + } + + return terminator.bind(state, callback); +} + + +/***/ }), + +/***/ 102: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +// For internal use, subject to change. +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +// We use any as a valid input type +/* eslint-disable @typescript-eslint/no-explicit-any */ +const fs = __importStar(__webpack_require__(747)); +const os = __importStar(__webpack_require__(87)); +const utils_1 = __webpack_require__(82); +function issueCommand(command, message) { + const filePath = process.env[`GITHUB_${command}`]; + if (!filePath) { + throw new Error(`Unable to find environment variable for file command ${command}`); + } + if (!fs.existsSync(filePath)) { + throw new Error(`Missing file at path: ${filePath}`); + } + fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { + encoding: 'utf8' + }); +} +exports.issueCommand = issueCommand; +//# sourceMappingURL=file-command.js.map + +/***/ }), + +/***/ 104: +/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) { + +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const core = __webpack_require__(470); +const formData = __webpack_require__(416); +const Mailgun = __webpack_require__(618); + +const mailgun = new Mailgun(formData); +const optionalFields = ['cc', 'text', 'html']; + +function loadConfig() { + return { + apiKey: core.getInput('api-key'), + domain: core.getInput('domain'), + to: core.getInput('to'), + from: core.getInput('from'), + cc: core.getInput('cc'), + subject: core.getInput('subject'), + text: core.getInput('text'), + html: core.getInput('html'), + } +} + +function validate(config) { + for (param in config) { + if (optionalFields.includes(param)) { + continue; + } + validateRequiredParameter(config[param], `'${param}'`); + } +} + +function validateRequiredParameter(value, name) { + if (!isNonEmptyString(value)) { + throw new Error(`${name} must be a non-empty string.`); + } +} + +function sendEmail(config) { + const mg = mailgun.client({ + username: 'api', + key: config.apiKey, + }); + + return mg.messages + .create(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); + + +/***/ }), + +/***/ 118: +/***/ (function(module) { + +module.exports = {"application/1d-interleaved-parityfec":{"source":"iana"},"application/3gpdash-qoe-report+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/3gpp-ims+xml":{"source":"iana","compressible":true},"application/a2l":{"source":"iana"},"application/activemessage":{"source":"iana"},"application/activity+json":{"source":"iana","compressible":true},"application/alto-costmap+json":{"source":"iana","compressible":true},"application/alto-costmapfilter+json":{"source":"iana","compressible":true},"application/alto-directory+json":{"source":"iana","compressible":true},"application/alto-endpointcost+json":{"source":"iana","compressible":true},"application/alto-endpointcostparams+json":{"source":"iana","compressible":true},"application/alto-endpointprop+json":{"source":"iana","compressible":true},"application/alto-endpointpropparams+json":{"source":"iana","compressible":true},"application/alto-error+json":{"source":"iana","compressible":true},"application/alto-networkmap+json":{"source":"iana","compressible":true},"application/alto-networkmapfilter+json":{"source":"iana","compressible":true},"application/alto-updatestreamcontrol+json":{"source":"iana","compressible":true},"application/alto-updatestreamparams+json":{"source":"iana","compressible":true},"application/aml":{"source":"iana"},"application/andrew-inset":{"source":"iana","extensions":["ez"]},"application/applefile":{"source":"iana"},"application/applixware":{"source":"apache","extensions":["aw"]},"application/atf":{"source":"iana"},"application/atfx":{"source":"iana"},"application/atom+xml":{"source":"iana","compressible":true,"extensions":["atom"]},"application/atomcat+xml":{"source":"iana","compressible":true,"extensions":["atomcat"]},"application/atomdeleted+xml":{"source":"iana","compressible":true,"extensions":["atomdeleted"]},"application/atomicmail":{"source":"iana"},"application/atomsvc+xml":{"source":"iana","compressible":true,"extensions":["atomsvc"]},"application/atsc-dwd+xml":{"source":"iana","compressible":true,"extensions":["dwd"]},"application/atsc-dynamic-event-message":{"source":"iana"},"application/atsc-held+xml":{"source":"iana","compressible":true,"extensions":["held"]},"application/atsc-rdt+json":{"source":"iana","compressible":true},"application/atsc-rsat+xml":{"source":"iana","compressible":true,"extensions":["rsat"]},"application/atxml":{"source":"iana"},"application/auth-policy+xml":{"source":"iana","compressible":true},"application/bacnet-xdd+zip":{"source":"iana","compressible":false},"application/batch-smtp":{"source":"iana"},"application/bdoc":{"compressible":false,"extensions":["bdoc"]},"application/beep+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/calendar+json":{"source":"iana","compressible":true},"application/calendar+xml":{"source":"iana","compressible":true,"extensions":["xcs"]},"application/call-completion":{"source":"iana"},"application/cals-1840":{"source":"iana"},"application/cap+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/cbor":{"source":"iana"},"application/cbor-seq":{"source":"iana"},"application/cccex":{"source":"iana"},"application/ccmp+xml":{"source":"iana","compressible":true},"application/ccxml+xml":{"source":"iana","compressible":true,"extensions":["ccxml"]},"application/cdfx+xml":{"source":"iana","compressible":true,"extensions":["cdfx"]},"application/cdmi-capability":{"source":"iana","extensions":["cdmia"]},"application/cdmi-container":{"source":"iana","extensions":["cdmic"]},"application/cdmi-domain":{"source":"iana","extensions":["cdmid"]},"application/cdmi-object":{"source":"iana","extensions":["cdmio"]},"application/cdmi-queue":{"source":"iana","extensions":["cdmiq"]},"application/cdni":{"source":"iana"},"application/cea":{"source":"iana"},"application/cea-2018+xml":{"source":"iana","compressible":true},"application/cellml+xml":{"source":"iana","compressible":true},"application/cfw":{"source":"iana"},"application/clue+xml":{"source":"iana","compressible":true},"application/clue_info+xml":{"source":"iana","compressible":true},"application/cms":{"source":"iana"},"application/cnrp+xml":{"source":"iana","compressible":true},"application/coap-group+json":{"source":"iana","compressible":true},"application/coap-payload":{"source":"iana"},"application/commonground":{"source":"iana"},"application/conference-info+xml":{"source":"iana","compressible":true},"application/cose":{"source":"iana"},"application/cose-key":{"source":"iana"},"application/cose-key-set":{"source":"iana"},"application/cpl+xml":{"source":"iana","compressible":true},"application/csrattrs":{"source":"iana"},"application/csta+xml":{"source":"iana","compressible":true},"application/cstadata+xml":{"source":"iana","compressible":true},"application/csvm+json":{"source":"iana","compressible":true},"application/cu-seeme":{"source":"apache","extensions":["cu"]},"application/cwt":{"source":"iana"},"application/cybercash":{"source":"iana"},"application/dart":{"compressible":true},"application/dash+xml":{"source":"iana","compressible":true,"extensions":["mpd"]},"application/dashdelta":{"source":"iana"},"application/davmount+xml":{"source":"iana","compressible":true,"extensions":["davmount"]},"application/dca-rft":{"source":"iana"},"application/dcd":{"source":"iana"},"application/dec-dx":{"source":"iana"},"application/dialog-info+xml":{"source":"iana","compressible":true},"application/dicom":{"source":"iana"},"application/dicom+json":{"source":"iana","compressible":true},"application/dicom+xml":{"source":"iana","compressible":true},"application/dii":{"source":"iana"},"application/dit":{"source":"iana"},"application/dns":{"source":"iana"},"application/dns+json":{"source":"iana","compressible":true},"application/dns-message":{"source":"iana"},"application/docbook+xml":{"source":"apache","compressible":true,"extensions":["dbk"]},"application/dots+cbor":{"source":"iana"},"application/dskpp+xml":{"source":"iana","compressible":true},"application/dssc+der":{"source":"iana","extensions":["dssc"]},"application/dssc+xml":{"source":"iana","compressible":true,"extensions":["xdssc"]},"application/dvcs":{"source":"iana"},"application/ecmascript":{"source":"iana","compressible":true,"extensions":["ecma","es"]},"application/edi-consent":{"source":"iana"},"application/edi-x12":{"source":"iana","compressible":false},"application/edifact":{"source":"iana","compressible":false},"application/efi":{"source":"iana"},"application/emergencycalldata.comment+xml":{"source":"iana","compressible":true},"application/emergencycalldata.control+xml":{"source":"iana","compressible":true},"application/emergencycalldata.deviceinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.ecall.msd":{"source":"iana"},"application/emergencycalldata.providerinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.serviceinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.subscriberinfo+xml":{"source":"iana","compressible":true},"application/emergencycalldata.veds+xml":{"source":"iana","compressible":true},"application/emma+xml":{"source":"iana","compressible":true,"extensions":["emma"]},"application/emotionml+xml":{"source":"iana","compressible":true,"extensions":["emotionml"]},"application/encaprtp":{"source":"iana"},"application/epp+xml":{"source":"iana","compressible":true},"application/epub+zip":{"source":"iana","compressible":false,"extensions":["epub"]},"application/eshop":{"source":"iana"},"application/exi":{"source":"iana","extensions":["exi"]},"application/expect-ct-report+json":{"source":"iana","compressible":true},"application/fastinfoset":{"source":"iana"},"application/fastsoap":{"source":"iana"},"application/fdt+xml":{"source":"iana","compressible":true,"extensions":["fdt"]},"application/fhir+json":{"source":"iana","charset":"UTF-8","compressible":true},"application/fhir+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/fido.trusted-apps+json":{"compressible":true},"application/fits":{"source":"iana"},"application/flexfec":{"source":"iana"},"application/font-sfnt":{"source":"iana"},"application/font-tdpfr":{"source":"iana","extensions":["pfr"]},"application/font-woff":{"source":"iana","compressible":false},"application/framework-attributes+xml":{"source":"iana","compressible":true},"application/geo+json":{"source":"iana","compressible":true,"extensions":["geojson"]},"application/geo+json-seq":{"source":"iana"},"application/geopackage+sqlite3":{"source":"iana"},"application/geoxacml+xml":{"source":"iana","compressible":true},"application/gltf-buffer":{"source":"iana"},"application/gml+xml":{"source":"iana","compressible":true,"extensions":["gml"]},"application/gpx+xml":{"source":"apache","compressible":true,"extensions":["gpx"]},"application/gxf":{"source":"apache","extensions":["gxf"]},"application/gzip":{"source":"iana","compressible":false,"extensions":["gz"]},"application/h224":{"source":"iana"},"application/held+xml":{"source":"iana","compressible":true},"application/hjson":{"extensions":["hjson"]},"application/http":{"source":"iana"},"application/hyperstudio":{"source":"iana","extensions":["stk"]},"application/ibe-key-request+xml":{"source":"iana","compressible":true},"application/ibe-pkg-reply+xml":{"source":"iana","compressible":true},"application/ibe-pp-data":{"source":"iana"},"application/iges":{"source":"iana"},"application/im-iscomposing+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/index":{"source":"iana"},"application/index.cmd":{"source":"iana"},"application/index.obj":{"source":"iana"},"application/index.response":{"source":"iana"},"application/index.vnd":{"source":"iana"},"application/inkml+xml":{"source":"iana","compressible":true,"extensions":["ink","inkml"]},"application/iotp":{"source":"iana"},"application/ipfix":{"source":"iana","extensions":["ipfix"]},"application/ipp":{"source":"iana"},"application/isup":{"source":"iana"},"application/its+xml":{"source":"iana","compressible":true,"extensions":["its"]},"application/java-archive":{"source":"apache","compressible":false,"extensions":["jar","war","ear"]},"application/java-serialized-object":{"source":"apache","compressible":false,"extensions":["ser"]},"application/java-vm":{"source":"apache","compressible":false,"extensions":["class"]},"application/javascript":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["js","mjs"]},"application/jf2feed+json":{"source":"iana","compressible":true},"application/jose":{"source":"iana"},"application/jose+json":{"source":"iana","compressible":true},"application/jrd+json":{"source":"iana","compressible":true},"application/json":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["json","map"]},"application/json-patch+json":{"source":"iana","compressible":true},"application/json-seq":{"source":"iana"},"application/json5":{"extensions":["json5"]},"application/jsonml+json":{"source":"apache","compressible":true,"extensions":["jsonml"]},"application/jwk+json":{"source":"iana","compressible":true},"application/jwk-set+json":{"source":"iana","compressible":true},"application/jwt":{"source":"iana"},"application/kpml-request+xml":{"source":"iana","compressible":true},"application/kpml-response+xml":{"source":"iana","compressible":true},"application/ld+json":{"source":"iana","compressible":true,"extensions":["jsonld"]},"application/lgr+xml":{"source":"iana","compressible":true,"extensions":["lgr"]},"application/link-format":{"source":"iana"},"application/load-control+xml":{"source":"iana","compressible":true},"application/lost+xml":{"source":"iana","compressible":true,"extensions":["lostxml"]},"application/lostsync+xml":{"source":"iana","compressible":true},"application/lpf+zip":{"source":"iana","compressible":false},"application/lxf":{"source":"iana"},"application/mac-binhex40":{"source":"iana","extensions":["hqx"]},"application/mac-compactpro":{"source":"apache","extensions":["cpt"]},"application/macwriteii":{"source":"iana"},"application/mads+xml":{"source":"iana","compressible":true,"extensions":["mads"]},"application/manifest+json":{"charset":"UTF-8","compressible":true,"extensions":["webmanifest"]},"application/marc":{"source":"iana","extensions":["mrc"]},"application/marcxml+xml":{"source":"iana","compressible":true,"extensions":["mrcx"]},"application/mathematica":{"source":"iana","extensions":["ma","nb","mb"]},"application/mathml+xml":{"source":"iana","compressible":true,"extensions":["mathml"]},"application/mathml-content+xml":{"source":"iana","compressible":true},"application/mathml-presentation+xml":{"source":"iana","compressible":true},"application/mbms-associated-procedure-description+xml":{"source":"iana","compressible":true},"application/mbms-deregister+xml":{"source":"iana","compressible":true},"application/mbms-envelope+xml":{"source":"iana","compressible":true},"application/mbms-msk+xml":{"source":"iana","compressible":true},"application/mbms-msk-response+xml":{"source":"iana","compressible":true},"application/mbms-protection-description+xml":{"source":"iana","compressible":true},"application/mbms-reception-report+xml":{"source":"iana","compressible":true},"application/mbms-register+xml":{"source":"iana","compressible":true},"application/mbms-register-response+xml":{"source":"iana","compressible":true},"application/mbms-schedule+xml":{"source":"iana","compressible":true},"application/mbms-user-service-description+xml":{"source":"iana","compressible":true},"application/mbox":{"source":"iana","extensions":["mbox"]},"application/media-policy-dataset+xml":{"source":"iana","compressible":true},"application/media_control+xml":{"source":"iana","compressible":true},"application/mediaservercontrol+xml":{"source":"iana","compressible":true,"extensions":["mscml"]},"application/merge-patch+json":{"source":"iana","compressible":true},"application/metalink+xml":{"source":"apache","compressible":true,"extensions":["metalink"]},"application/metalink4+xml":{"source":"iana","compressible":true,"extensions":["meta4"]},"application/mets+xml":{"source":"iana","compressible":true,"extensions":["mets"]},"application/mf4":{"source":"iana"},"application/mikey":{"source":"iana"},"application/mipc":{"source":"iana"},"application/mmt-aei+xml":{"source":"iana","compressible":true,"extensions":["maei"]},"application/mmt-usd+xml":{"source":"iana","compressible":true,"extensions":["musd"]},"application/mods+xml":{"source":"iana","compressible":true,"extensions":["mods"]},"application/moss-keys":{"source":"iana"},"application/moss-signature":{"source":"iana"},"application/mosskey-data":{"source":"iana"},"application/mosskey-request":{"source":"iana"},"application/mp21":{"source":"iana","extensions":["m21","mp21"]},"application/mp4":{"source":"iana","extensions":["mp4s","m4p"]},"application/mpeg4-generic":{"source":"iana"},"application/mpeg4-iod":{"source":"iana"},"application/mpeg4-iod-xmt":{"source":"iana"},"application/mrb-consumer+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/mrb-publish+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/msc-ivr+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/msc-mixer+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/msword":{"source":"iana","compressible":false,"extensions":["doc","dot"]},"application/mud+json":{"source":"iana","compressible":true},"application/multipart-core":{"source":"iana"},"application/mxf":{"source":"iana","extensions":["mxf"]},"application/n-quads":{"source":"iana","extensions":["nq"]},"application/n-triples":{"source":"iana","extensions":["nt"]},"application/nasdata":{"source":"iana"},"application/news-checkgroups":{"source":"iana","charset":"US-ASCII"},"application/news-groupinfo":{"source":"iana","charset":"US-ASCII"},"application/news-transmission":{"source":"iana"},"application/nlsml+xml":{"source":"iana","compressible":true},"application/node":{"source":"iana","extensions":["cjs"]},"application/nss":{"source":"iana"},"application/ocsp-request":{"source":"iana"},"application/ocsp-response":{"source":"iana"},"application/octet-stream":{"source":"iana","compressible":false,"extensions":["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"]},"application/oda":{"source":"iana","extensions":["oda"]},"application/odm+xml":{"source":"iana","compressible":true},"application/odx":{"source":"iana"},"application/oebps-package+xml":{"source":"iana","compressible":true,"extensions":["opf"]},"application/ogg":{"source":"iana","compressible":false,"extensions":["ogx"]},"application/omdoc+xml":{"source":"apache","compressible":true,"extensions":["omdoc"]},"application/onenote":{"source":"apache","extensions":["onetoc","onetoc2","onetmp","onepkg"]},"application/oscore":{"source":"iana"},"application/oxps":{"source":"iana","extensions":["oxps"]},"application/p2p-overlay+xml":{"source":"iana","compressible":true,"extensions":["relo"]},"application/parityfec":{"source":"iana"},"application/passport":{"source":"iana"},"application/patch-ops-error+xml":{"source":"iana","compressible":true,"extensions":["xer"]},"application/pdf":{"source":"iana","compressible":false,"extensions":["pdf"]},"application/pdx":{"source":"iana"},"application/pem-certificate-chain":{"source":"iana"},"application/pgp-encrypted":{"source":"iana","compressible":false,"extensions":["pgp"]},"application/pgp-keys":{"source":"iana"},"application/pgp-signature":{"source":"iana","extensions":["asc","sig"]},"application/pics-rules":{"source":"apache","extensions":["prf"]},"application/pidf+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/pidf-diff+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/pkcs10":{"source":"iana","extensions":["p10"]},"application/pkcs12":{"source":"iana"},"application/pkcs7-mime":{"source":"iana","extensions":["p7m","p7c"]},"application/pkcs7-signature":{"source":"iana","extensions":["p7s"]},"application/pkcs8":{"source":"iana","extensions":["p8"]},"application/pkcs8-encrypted":{"source":"iana"},"application/pkix-attr-cert":{"source":"iana","extensions":["ac"]},"application/pkix-cert":{"source":"iana","extensions":["cer"]},"application/pkix-crl":{"source":"iana","extensions":["crl"]},"application/pkix-pkipath":{"source":"iana","extensions":["pkipath"]},"application/pkixcmp":{"source":"iana","extensions":["pki"]},"application/pls+xml":{"source":"iana","compressible":true,"extensions":["pls"]},"application/poc-settings+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/postscript":{"source":"iana","compressible":true,"extensions":["ai","eps","ps"]},"application/ppsp-tracker+json":{"source":"iana","compressible":true},"application/problem+json":{"source":"iana","compressible":true},"application/problem+xml":{"source":"iana","compressible":true},"application/provenance+xml":{"source":"iana","compressible":true,"extensions":["provx"]},"application/prs.alvestrand.titrax-sheet":{"source":"iana"},"application/prs.cww":{"source":"iana","extensions":["cww"]},"application/prs.hpub+zip":{"source":"iana","compressible":false},"application/prs.nprend":{"source":"iana"},"application/prs.plucker":{"source":"iana"},"application/prs.rdf-xml-crypt":{"source":"iana"},"application/prs.xsf+xml":{"source":"iana","compressible":true},"application/pskc+xml":{"source":"iana","compressible":true,"extensions":["pskcxml"]},"application/pvd+json":{"source":"iana","compressible":true},"application/qsig":{"source":"iana"},"application/raml+yaml":{"compressible":true,"extensions":["raml"]},"application/raptorfec":{"source":"iana"},"application/rdap+json":{"source":"iana","compressible":true},"application/rdf+xml":{"source":"iana","compressible":true,"extensions":["rdf","owl"]},"application/reginfo+xml":{"source":"iana","compressible":true,"extensions":["rif"]},"application/relax-ng-compact-syntax":{"source":"iana","extensions":["rnc"]},"application/remote-printing":{"source":"iana"},"application/reputon+json":{"source":"iana","compressible":true},"application/resource-lists+xml":{"source":"iana","compressible":true,"extensions":["rl"]},"application/resource-lists-diff+xml":{"source":"iana","compressible":true,"extensions":["rld"]},"application/rfc+xml":{"source":"iana","compressible":true},"application/riscos":{"source":"iana"},"application/rlmi+xml":{"source":"iana","compressible":true},"application/rls-services+xml":{"source":"iana","compressible":true,"extensions":["rs"]},"application/route-apd+xml":{"source":"iana","compressible":true,"extensions":["rapd"]},"application/route-s-tsid+xml":{"source":"iana","compressible":true,"extensions":["sls"]},"application/route-usd+xml":{"source":"iana","compressible":true,"extensions":["rusd"]},"application/rpki-ghostbusters":{"source":"iana","extensions":["gbr"]},"application/rpki-manifest":{"source":"iana","extensions":["mft"]},"application/rpki-publication":{"source":"iana"},"application/rpki-roa":{"source":"iana","extensions":["roa"]},"application/rpki-updown":{"source":"iana"},"application/rsd+xml":{"source":"apache","compressible":true,"extensions":["rsd"]},"application/rss+xml":{"source":"apache","compressible":true,"extensions":["rss"]},"application/rtf":{"source":"iana","compressible":true,"extensions":["rtf"]},"application/rtploopback":{"source":"iana"},"application/rtx":{"source":"iana"},"application/samlassertion+xml":{"source":"iana","compressible":true},"application/samlmetadata+xml":{"source":"iana","compressible":true},"application/sbe":{"source":"iana"},"application/sbml+xml":{"source":"iana","compressible":true,"extensions":["sbml"]},"application/scaip+xml":{"source":"iana","compressible":true},"application/scim+json":{"source":"iana","compressible":true},"application/scvp-cv-request":{"source":"iana","extensions":["scq"]},"application/scvp-cv-response":{"source":"iana","extensions":["scs"]},"application/scvp-vp-request":{"source":"iana","extensions":["spq"]},"application/scvp-vp-response":{"source":"iana","extensions":["spp"]},"application/sdp":{"source":"iana","extensions":["sdp"]},"application/secevent+jwt":{"source":"iana"},"application/senml+cbor":{"source":"iana"},"application/senml+json":{"source":"iana","compressible":true},"application/senml+xml":{"source":"iana","compressible":true,"extensions":["senmlx"]},"application/senml-etch+cbor":{"source":"iana"},"application/senml-etch+json":{"source":"iana","compressible":true},"application/senml-exi":{"source":"iana"},"application/sensml+cbor":{"source":"iana"},"application/sensml+json":{"source":"iana","compressible":true},"application/sensml+xml":{"source":"iana","compressible":true,"extensions":["sensmlx"]},"application/sensml-exi":{"source":"iana"},"application/sep+xml":{"source":"iana","compressible":true},"application/sep-exi":{"source":"iana"},"application/session-info":{"source":"iana"},"application/set-payment":{"source":"iana"},"application/set-payment-initiation":{"source":"iana","extensions":["setpay"]},"application/set-registration":{"source":"iana"},"application/set-registration-initiation":{"source":"iana","extensions":["setreg"]},"application/sgml":{"source":"iana"},"application/sgml-open-catalog":{"source":"iana"},"application/shf+xml":{"source":"iana","compressible":true,"extensions":["shf"]},"application/sieve":{"source":"iana","extensions":["siv","sieve"]},"application/simple-filter+xml":{"source":"iana","compressible":true},"application/simple-message-summary":{"source":"iana"},"application/simplesymbolcontainer":{"source":"iana"},"application/sipc":{"source":"iana"},"application/slate":{"source":"iana"},"application/smil":{"source":"iana"},"application/smil+xml":{"source":"iana","compressible":true,"extensions":["smi","smil"]},"application/smpte336m":{"source":"iana"},"application/soap+fastinfoset":{"source":"iana"},"application/soap+xml":{"source":"iana","compressible":true},"application/sparql-query":{"source":"iana","extensions":["rq"]},"application/sparql-results+xml":{"source":"iana","compressible":true,"extensions":["srx"]},"application/spirits-event+xml":{"source":"iana","compressible":true},"application/sql":{"source":"iana"},"application/srgs":{"source":"iana","extensions":["gram"]},"application/srgs+xml":{"source":"iana","compressible":true,"extensions":["grxml"]},"application/sru+xml":{"source":"iana","compressible":true,"extensions":["sru"]},"application/ssdl+xml":{"source":"apache","compressible":true,"extensions":["ssdl"]},"application/ssml+xml":{"source":"iana","compressible":true,"extensions":["ssml"]},"application/stix+json":{"source":"iana","compressible":true},"application/swid+xml":{"source":"iana","compressible":true,"extensions":["swidtag"]},"application/tamp-apex-update":{"source":"iana"},"application/tamp-apex-update-confirm":{"source":"iana"},"application/tamp-community-update":{"source":"iana"},"application/tamp-community-update-confirm":{"source":"iana"},"application/tamp-error":{"source":"iana"},"application/tamp-sequence-adjust":{"source":"iana"},"application/tamp-sequence-adjust-confirm":{"source":"iana"},"application/tamp-status-query":{"source":"iana"},"application/tamp-status-response":{"source":"iana"},"application/tamp-update":{"source":"iana"},"application/tamp-update-confirm":{"source":"iana"},"application/tar":{"compressible":true},"application/taxii+json":{"source":"iana","compressible":true},"application/td+json":{"source":"iana","compressible":true},"application/tei+xml":{"source":"iana","compressible":true,"extensions":["tei","teicorpus"]},"application/tetra_isi":{"source":"iana"},"application/thraud+xml":{"source":"iana","compressible":true,"extensions":["tfi"]},"application/timestamp-query":{"source":"iana"},"application/timestamp-reply":{"source":"iana"},"application/timestamped-data":{"source":"iana","extensions":["tsd"]},"application/tlsrpt+gzip":{"source":"iana"},"application/tlsrpt+json":{"source":"iana","compressible":true},"application/tnauthlist":{"source":"iana"},"application/toml":{"compressible":true,"extensions":["toml"]},"application/trickle-ice-sdpfrag":{"source":"iana"},"application/trig":{"source":"iana"},"application/ttml+xml":{"source":"iana","compressible":true,"extensions":["ttml"]},"application/tve-trigger":{"source":"iana"},"application/tzif":{"source":"iana"},"application/tzif-leap":{"source":"iana"},"application/ulpfec":{"source":"iana"},"application/urc-grpsheet+xml":{"source":"iana","compressible":true},"application/urc-ressheet+xml":{"source":"iana","compressible":true,"extensions":["rsheet"]},"application/urc-targetdesc+xml":{"source":"iana","compressible":true},"application/urc-uisocketdesc+xml":{"source":"iana","compressible":true},"application/vcard+json":{"source":"iana","compressible":true},"application/vcard+xml":{"source":"iana","compressible":true},"application/vemmi":{"source":"iana"},"application/vividence.scriptfile":{"source":"apache"},"application/vnd.1000minds.decision-model+xml":{"source":"iana","compressible":true,"extensions":["1km"]},"application/vnd.3gpp-prose+xml":{"source":"iana","compressible":true},"application/vnd.3gpp-prose-pc3ch+xml":{"source":"iana","compressible":true},"application/vnd.3gpp-v2x-local-service-information":{"source":"iana"},"application/vnd.3gpp.access-transfer-events+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.bsf+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.gmop+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mc-signalling-ear":{"source":"iana"},"application/vnd.3gpp.mcdata-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-payload":{"source":"iana"},"application/vnd.3gpp.mcdata-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-signalling":{"source":"iana"},"application/vnd.3gpp.mcdata-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcdata-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-floor-request+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-location-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-mbms-usage-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-signed+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-ue-init-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcptt-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-affiliation-command+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-affiliation-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-location-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-mbms-usage-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-service-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-transmission-request+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-ue-config+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mcvideo-user-profile+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.mid-call+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.pic-bw-large":{"source":"iana","extensions":["plb"]},"application/vnd.3gpp.pic-bw-small":{"source":"iana","extensions":["psb"]},"application/vnd.3gpp.pic-bw-var":{"source":"iana","extensions":["pvb"]},"application/vnd.3gpp.sms":{"source":"iana"},"application/vnd.3gpp.sms+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.srvcc-ext+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.srvcc-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.state-and-event-info+xml":{"source":"iana","compressible":true},"application/vnd.3gpp.ussd+xml":{"source":"iana","compressible":true},"application/vnd.3gpp2.bcmcsinfo+xml":{"source":"iana","compressible":true},"application/vnd.3gpp2.sms":{"source":"iana"},"application/vnd.3gpp2.tcap":{"source":"iana","extensions":["tcap"]},"application/vnd.3lightssoftware.imagescal":{"source":"iana"},"application/vnd.3m.post-it-notes":{"source":"iana","extensions":["pwn"]},"application/vnd.accpac.simply.aso":{"source":"iana","extensions":["aso"]},"application/vnd.accpac.simply.imp":{"source":"iana","extensions":["imp"]},"application/vnd.acucobol":{"source":"iana","extensions":["acu"]},"application/vnd.acucorp":{"source":"iana","extensions":["atc","acutc"]},"application/vnd.adobe.air-application-installer-package+zip":{"source":"apache","compressible":false,"extensions":["air"]},"application/vnd.adobe.flash.movie":{"source":"iana"},"application/vnd.adobe.formscentral.fcdt":{"source":"iana","extensions":["fcdt"]},"application/vnd.adobe.fxp":{"source":"iana","extensions":["fxp","fxpl"]},"application/vnd.adobe.partial-upload":{"source":"iana"},"application/vnd.adobe.xdp+xml":{"source":"iana","compressible":true,"extensions":["xdp"]},"application/vnd.adobe.xfdf":{"source":"iana","extensions":["xfdf"]},"application/vnd.aether.imp":{"source":"iana"},"application/vnd.afpc.afplinedata":{"source":"iana"},"application/vnd.afpc.afplinedata-pagedef":{"source":"iana"},"application/vnd.afpc.foca-charset":{"source":"iana"},"application/vnd.afpc.foca-codedfont":{"source":"iana"},"application/vnd.afpc.foca-codepage":{"source":"iana"},"application/vnd.afpc.modca":{"source":"iana"},"application/vnd.afpc.modca-formdef":{"source":"iana"},"application/vnd.afpc.modca-mediummap":{"source":"iana"},"application/vnd.afpc.modca-objectcontainer":{"source":"iana"},"application/vnd.afpc.modca-overlay":{"source":"iana"},"application/vnd.afpc.modca-pagesegment":{"source":"iana"},"application/vnd.ah-barcode":{"source":"iana"},"application/vnd.ahead.space":{"source":"iana","extensions":["ahead"]},"application/vnd.airzip.filesecure.azf":{"source":"iana","extensions":["azf"]},"application/vnd.airzip.filesecure.azs":{"source":"iana","extensions":["azs"]},"application/vnd.amadeus+json":{"source":"iana","compressible":true},"application/vnd.amazon.ebook":{"source":"apache","extensions":["azw"]},"application/vnd.amazon.mobi8-ebook":{"source":"iana"},"application/vnd.americandynamics.acc":{"source":"iana","extensions":["acc"]},"application/vnd.amiga.ami":{"source":"iana","extensions":["ami"]},"application/vnd.amundsen.maze+xml":{"source":"iana","compressible":true},"application/vnd.android.ota":{"source":"iana"},"application/vnd.android.package-archive":{"source":"apache","compressible":false,"extensions":["apk"]},"application/vnd.anki":{"source":"iana"},"application/vnd.anser-web-certificate-issue-initiation":{"source":"iana","extensions":["cii"]},"application/vnd.anser-web-funds-transfer-initiation":{"source":"apache","extensions":["fti"]},"application/vnd.antix.game-component":{"source":"iana","extensions":["atx"]},"application/vnd.apache.thrift.binary":{"source":"iana"},"application/vnd.apache.thrift.compact":{"source":"iana"},"application/vnd.apache.thrift.json":{"source":"iana"},"application/vnd.api+json":{"source":"iana","compressible":true},"application/vnd.aplextor.warrp+json":{"source":"iana","compressible":true},"application/vnd.apothekende.reservation+json":{"source":"iana","compressible":true},"application/vnd.apple.installer+xml":{"source":"iana","compressible":true,"extensions":["mpkg"]},"application/vnd.apple.keynote":{"source":"iana","extensions":["keynote"]},"application/vnd.apple.mpegurl":{"source":"iana","extensions":["m3u8"]},"application/vnd.apple.numbers":{"source":"iana","extensions":["numbers"]},"application/vnd.apple.pages":{"source":"iana","extensions":["pages"]},"application/vnd.apple.pkpass":{"compressible":false,"extensions":["pkpass"]},"application/vnd.arastra.swi":{"source":"iana"},"application/vnd.aristanetworks.swi":{"source":"iana","extensions":["swi"]},"application/vnd.artisan+json":{"source":"iana","compressible":true},"application/vnd.artsquare":{"source":"iana"},"application/vnd.astraea-software.iota":{"source":"iana","extensions":["iota"]},"application/vnd.audiograph":{"source":"iana","extensions":["aep"]},"application/vnd.autopackage":{"source":"iana"},"application/vnd.avalon+json":{"source":"iana","compressible":true},"application/vnd.avistar+xml":{"source":"iana","compressible":true},"application/vnd.balsamiq.bmml+xml":{"source":"iana","compressible":true,"extensions":["bmml"]},"application/vnd.balsamiq.bmpr":{"source":"iana"},"application/vnd.banana-accounting":{"source":"iana"},"application/vnd.bbf.usp.error":{"source":"iana"},"application/vnd.bbf.usp.msg":{"source":"iana"},"application/vnd.bbf.usp.msg+json":{"source":"iana","compressible":true},"application/vnd.bekitzur-stech+json":{"source":"iana","compressible":true},"application/vnd.bint.med-content":{"source":"iana"},"application/vnd.biopax.rdf+xml":{"source":"iana","compressible":true},"application/vnd.blink-idb-value-wrapper":{"source":"iana"},"application/vnd.blueice.multipass":{"source":"iana","extensions":["mpm"]},"application/vnd.bluetooth.ep.oob":{"source":"iana"},"application/vnd.bluetooth.le.oob":{"source":"iana"},"application/vnd.bmi":{"source":"iana","extensions":["bmi"]},"application/vnd.bpf":{"source":"iana"},"application/vnd.bpf3":{"source":"iana"},"application/vnd.businessobjects":{"source":"iana","extensions":["rep"]},"application/vnd.byu.uapi+json":{"source":"iana","compressible":true},"application/vnd.cab-jscript":{"source":"iana"},"application/vnd.canon-cpdl":{"source":"iana"},"application/vnd.canon-lips":{"source":"iana"},"application/vnd.capasystems-pg+json":{"source":"iana","compressible":true},"application/vnd.cendio.thinlinc.clientconf":{"source":"iana"},"application/vnd.century-systems.tcp_stream":{"source":"iana"},"application/vnd.chemdraw+xml":{"source":"iana","compressible":true,"extensions":["cdxml"]},"application/vnd.chess-pgn":{"source":"iana"},"application/vnd.chipnuts.karaoke-mmd":{"source":"iana","extensions":["mmd"]},"application/vnd.ciedi":{"source":"iana"},"application/vnd.cinderella":{"source":"iana","extensions":["cdy"]},"application/vnd.cirpack.isdn-ext":{"source":"iana"},"application/vnd.citationstyles.style+xml":{"source":"iana","compressible":true,"extensions":["csl"]},"application/vnd.claymore":{"source":"iana","extensions":["cla"]},"application/vnd.cloanto.rp9":{"source":"iana","extensions":["rp9"]},"application/vnd.clonk.c4group":{"source":"iana","extensions":["c4g","c4d","c4f","c4p","c4u"]},"application/vnd.cluetrust.cartomobile-config":{"source":"iana","extensions":["c11amc"]},"application/vnd.cluetrust.cartomobile-config-pkg":{"source":"iana","extensions":["c11amz"]},"application/vnd.coffeescript":{"source":"iana"},"application/vnd.collabio.xodocuments.document":{"source":"iana"},"application/vnd.collabio.xodocuments.document-template":{"source":"iana"},"application/vnd.collabio.xodocuments.presentation":{"source":"iana"},"application/vnd.collabio.xodocuments.presentation-template":{"source":"iana"},"application/vnd.collabio.xodocuments.spreadsheet":{"source":"iana"},"application/vnd.collabio.xodocuments.spreadsheet-template":{"source":"iana"},"application/vnd.collection+json":{"source":"iana","compressible":true},"application/vnd.collection.doc+json":{"source":"iana","compressible":true},"application/vnd.collection.next+json":{"source":"iana","compressible":true},"application/vnd.comicbook+zip":{"source":"iana","compressible":false},"application/vnd.comicbook-rar":{"source":"iana"},"application/vnd.commerce-battelle":{"source":"iana"},"application/vnd.commonspace":{"source":"iana","extensions":["csp"]},"application/vnd.contact.cmsg":{"source":"iana","extensions":["cdbcmsg"]},"application/vnd.coreos.ignition+json":{"source":"iana","compressible":true},"application/vnd.cosmocaller":{"source":"iana","extensions":["cmc"]},"application/vnd.crick.clicker":{"source":"iana","extensions":["clkx"]},"application/vnd.crick.clicker.keyboard":{"source":"iana","extensions":["clkk"]},"application/vnd.crick.clicker.palette":{"source":"iana","extensions":["clkp"]},"application/vnd.crick.clicker.template":{"source":"iana","extensions":["clkt"]},"application/vnd.crick.clicker.wordbank":{"source":"iana","extensions":["clkw"]},"application/vnd.criticaltools.wbs+xml":{"source":"iana","compressible":true,"extensions":["wbs"]},"application/vnd.cryptii.pipe+json":{"source":"iana","compressible":true},"application/vnd.crypto-shade-file":{"source":"iana"},"application/vnd.ctc-posml":{"source":"iana","extensions":["pml"]},"application/vnd.ctct.ws+xml":{"source":"iana","compressible":true},"application/vnd.cups-pdf":{"source":"iana"},"application/vnd.cups-postscript":{"source":"iana"},"application/vnd.cups-ppd":{"source":"iana","extensions":["ppd"]},"application/vnd.cups-raster":{"source":"iana"},"application/vnd.cups-raw":{"source":"iana"},"application/vnd.curl":{"source":"iana"},"application/vnd.curl.car":{"source":"apache","extensions":["car"]},"application/vnd.curl.pcurl":{"source":"apache","extensions":["pcurl"]},"application/vnd.cyan.dean.root+xml":{"source":"iana","compressible":true},"application/vnd.cybank":{"source":"iana"},"application/vnd.d2l.coursepackage1p0+zip":{"source":"iana","compressible":false},"application/vnd.dart":{"source":"iana","compressible":true,"extensions":["dart"]},"application/vnd.data-vision.rdz":{"source":"iana","extensions":["rdz"]},"application/vnd.datapackage+json":{"source":"iana","compressible":true},"application/vnd.dataresource+json":{"source":"iana","compressible":true},"application/vnd.dbf":{"source":"iana"},"application/vnd.debian.binary-package":{"source":"iana"},"application/vnd.dece.data":{"source":"iana","extensions":["uvf","uvvf","uvd","uvvd"]},"application/vnd.dece.ttml+xml":{"source":"iana","compressible":true,"extensions":["uvt","uvvt"]},"application/vnd.dece.unspecified":{"source":"iana","extensions":["uvx","uvvx"]},"application/vnd.dece.zip":{"source":"iana","extensions":["uvz","uvvz"]},"application/vnd.denovo.fcselayout-link":{"source":"iana","extensions":["fe_launch"]},"application/vnd.desmume.movie":{"source":"iana"},"application/vnd.dir-bi.plate-dl-nosuffix":{"source":"iana"},"application/vnd.dm.delegation+xml":{"source":"iana","compressible":true},"application/vnd.dna":{"source":"iana","extensions":["dna"]},"application/vnd.document+json":{"source":"iana","compressible":true},"application/vnd.dolby.mlp":{"source":"apache","extensions":["mlp"]},"application/vnd.dolby.mobile.1":{"source":"iana"},"application/vnd.dolby.mobile.2":{"source":"iana"},"application/vnd.doremir.scorecloud-binary-document":{"source":"iana"},"application/vnd.dpgraph":{"source":"iana","extensions":["dpg"]},"application/vnd.dreamfactory":{"source":"iana","extensions":["dfac"]},"application/vnd.drive+json":{"source":"iana","compressible":true},"application/vnd.ds-keypoint":{"source":"apache","extensions":["kpxx"]},"application/vnd.dtg.local":{"source":"iana"},"application/vnd.dtg.local.flash":{"source":"iana"},"application/vnd.dtg.local.html":{"source":"iana"},"application/vnd.dvb.ait":{"source":"iana","extensions":["ait"]},"application/vnd.dvb.dvbisl+xml":{"source":"iana","compressible":true},"application/vnd.dvb.dvbj":{"source":"iana"},"application/vnd.dvb.esgcontainer":{"source":"iana"},"application/vnd.dvb.ipdcdftnotifaccess":{"source":"iana"},"application/vnd.dvb.ipdcesgaccess":{"source":"iana"},"application/vnd.dvb.ipdcesgaccess2":{"source":"iana"},"application/vnd.dvb.ipdcesgpdd":{"source":"iana"},"application/vnd.dvb.ipdcroaming":{"source":"iana"},"application/vnd.dvb.iptv.alfec-base":{"source":"iana"},"application/vnd.dvb.iptv.alfec-enhancement":{"source":"iana"},"application/vnd.dvb.notif-aggregate-root+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-container+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-generic+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-msglist+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-registration-request+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-ia-registration-response+xml":{"source":"iana","compressible":true},"application/vnd.dvb.notif-init+xml":{"source":"iana","compressible":true},"application/vnd.dvb.pfr":{"source":"iana"},"application/vnd.dvb.service":{"source":"iana","extensions":["svc"]},"application/vnd.dxr":{"source":"iana"},"application/vnd.dynageo":{"source":"iana","extensions":["geo"]},"application/vnd.dzr":{"source":"iana"},"application/vnd.easykaraoke.cdgdownload":{"source":"iana"},"application/vnd.ecdis-update":{"source":"iana"},"application/vnd.ecip.rlp":{"source":"iana"},"application/vnd.ecowin.chart":{"source":"iana","extensions":["mag"]},"application/vnd.ecowin.filerequest":{"source":"iana"},"application/vnd.ecowin.fileupdate":{"source":"iana"},"application/vnd.ecowin.series":{"source":"iana"},"application/vnd.ecowin.seriesrequest":{"source":"iana"},"application/vnd.ecowin.seriesupdate":{"source":"iana"},"application/vnd.efi.img":{"source":"iana"},"application/vnd.efi.iso":{"source":"iana"},"application/vnd.emclient.accessrequest+xml":{"source":"iana","compressible":true},"application/vnd.enliven":{"source":"iana","extensions":["nml"]},"application/vnd.enphase.envoy":{"source":"iana"},"application/vnd.eprints.data+xml":{"source":"iana","compressible":true},"application/vnd.epson.esf":{"source":"iana","extensions":["esf"]},"application/vnd.epson.msf":{"source":"iana","extensions":["msf"]},"application/vnd.epson.quickanime":{"source":"iana","extensions":["qam"]},"application/vnd.epson.salt":{"source":"iana","extensions":["slt"]},"application/vnd.epson.ssf":{"source":"iana","extensions":["ssf"]},"application/vnd.ericsson.quickcall":{"source":"iana"},"application/vnd.espass-espass+zip":{"source":"iana","compressible":false},"application/vnd.eszigno3+xml":{"source":"iana","compressible":true,"extensions":["es3","et3"]},"application/vnd.etsi.aoc+xml":{"source":"iana","compressible":true},"application/vnd.etsi.asic-e+zip":{"source":"iana","compressible":false},"application/vnd.etsi.asic-s+zip":{"source":"iana","compressible":false},"application/vnd.etsi.cug+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvcommand+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvdiscovery+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvprofile+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-bc+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-cod+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsad-npvr+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvservice+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvsync+xml":{"source":"iana","compressible":true},"application/vnd.etsi.iptvueprofile+xml":{"source":"iana","compressible":true},"application/vnd.etsi.mcid+xml":{"source":"iana","compressible":true},"application/vnd.etsi.mheg5":{"source":"iana"},"application/vnd.etsi.overload-control-policy-dataset+xml":{"source":"iana","compressible":true},"application/vnd.etsi.pstn+xml":{"source":"iana","compressible":true},"application/vnd.etsi.sci+xml":{"source":"iana","compressible":true},"application/vnd.etsi.simservs+xml":{"source":"iana","compressible":true},"application/vnd.etsi.timestamp-token":{"source":"iana"},"application/vnd.etsi.tsl+xml":{"source":"iana","compressible":true},"application/vnd.etsi.tsl.der":{"source":"iana"},"application/vnd.eudora.data":{"source":"iana"},"application/vnd.evolv.ecig.profile":{"source":"iana"},"application/vnd.evolv.ecig.settings":{"source":"iana"},"application/vnd.evolv.ecig.theme":{"source":"iana"},"application/vnd.exstream-empower+zip":{"source":"iana","compressible":false},"application/vnd.exstream-package":{"source":"iana"},"application/vnd.ezpix-album":{"source":"iana","extensions":["ez2"]},"application/vnd.ezpix-package":{"source":"iana","extensions":["ez3"]},"application/vnd.f-secure.mobile":{"source":"iana"},"application/vnd.fastcopy-disk-image":{"source":"iana"},"application/vnd.fdf":{"source":"iana","extensions":["fdf"]},"application/vnd.fdsn.mseed":{"source":"iana","extensions":["mseed"]},"application/vnd.fdsn.seed":{"source":"iana","extensions":["seed","dataless"]},"application/vnd.ffsns":{"source":"iana"},"application/vnd.ficlab.flb+zip":{"source":"iana","compressible":false},"application/vnd.filmit.zfc":{"source":"iana"},"application/vnd.fints":{"source":"iana"},"application/vnd.firemonkeys.cloudcell":{"source":"iana"},"application/vnd.flographit":{"source":"iana","extensions":["gph"]},"application/vnd.fluxtime.clip":{"source":"iana","extensions":["ftc"]},"application/vnd.font-fontforge-sfd":{"source":"iana"},"application/vnd.framemaker":{"source":"iana","extensions":["fm","frame","maker","book"]},"application/vnd.frogans.fnc":{"source":"iana","extensions":["fnc"]},"application/vnd.frogans.ltf":{"source":"iana","extensions":["ltf"]},"application/vnd.fsc.weblaunch":{"source":"iana","extensions":["fsc"]},"application/vnd.fujitsu.oasys":{"source":"iana","extensions":["oas"]},"application/vnd.fujitsu.oasys2":{"source":"iana","extensions":["oa2"]},"application/vnd.fujitsu.oasys3":{"source":"iana","extensions":["oa3"]},"application/vnd.fujitsu.oasysgp":{"source":"iana","extensions":["fg5"]},"application/vnd.fujitsu.oasysprs":{"source":"iana","extensions":["bh2"]},"application/vnd.fujixerox.art-ex":{"source":"iana"},"application/vnd.fujixerox.art4":{"source":"iana"},"application/vnd.fujixerox.ddd":{"source":"iana","extensions":["ddd"]},"application/vnd.fujixerox.docuworks":{"source":"iana","extensions":["xdw"]},"application/vnd.fujixerox.docuworks.binder":{"source":"iana","extensions":["xbd"]},"application/vnd.fujixerox.docuworks.container":{"source":"iana"},"application/vnd.fujixerox.hbpl":{"source":"iana"},"application/vnd.fut-misnet":{"source":"iana"},"application/vnd.futoin+cbor":{"source":"iana"},"application/vnd.futoin+json":{"source":"iana","compressible":true},"application/vnd.fuzzysheet":{"source":"iana","extensions":["fzs"]},"application/vnd.genomatix.tuxedo":{"source":"iana","extensions":["txd"]},"application/vnd.gentics.grd+json":{"source":"iana","compressible":true},"application/vnd.geo+json":{"source":"iana","compressible":true},"application/vnd.geocube+xml":{"source":"iana","compressible":true},"application/vnd.geogebra.file":{"source":"iana","extensions":["ggb"]},"application/vnd.geogebra.tool":{"source":"iana","extensions":["ggt"]},"application/vnd.geometry-explorer":{"source":"iana","extensions":["gex","gre"]},"application/vnd.geonext":{"source":"iana","extensions":["gxt"]},"application/vnd.geoplan":{"source":"iana","extensions":["g2w"]},"application/vnd.geospace":{"source":"iana","extensions":["g3w"]},"application/vnd.gerber":{"source":"iana"},"application/vnd.globalplatform.card-content-mgt":{"source":"iana"},"application/vnd.globalplatform.card-content-mgt-response":{"source":"iana"},"application/vnd.gmx":{"source":"iana","extensions":["gmx"]},"application/vnd.google-apps.document":{"compressible":false,"extensions":["gdoc"]},"application/vnd.google-apps.presentation":{"compressible":false,"extensions":["gslides"]},"application/vnd.google-apps.spreadsheet":{"compressible":false,"extensions":["gsheet"]},"application/vnd.google-earth.kml+xml":{"source":"iana","compressible":true,"extensions":["kml"]},"application/vnd.google-earth.kmz":{"source":"iana","compressible":false,"extensions":["kmz"]},"application/vnd.gov.sk.e-form+xml":{"source":"iana","compressible":true},"application/vnd.gov.sk.e-form+zip":{"source":"iana","compressible":false},"application/vnd.gov.sk.xmldatacontainer+xml":{"source":"iana","compressible":true},"application/vnd.grafeq":{"source":"iana","extensions":["gqf","gqs"]},"application/vnd.gridmp":{"source":"iana"},"application/vnd.groove-account":{"source":"iana","extensions":["gac"]},"application/vnd.groove-help":{"source":"iana","extensions":["ghf"]},"application/vnd.groove-identity-message":{"source":"iana","extensions":["gim"]},"application/vnd.groove-injector":{"source":"iana","extensions":["grv"]},"application/vnd.groove-tool-message":{"source":"iana","extensions":["gtm"]},"application/vnd.groove-tool-template":{"source":"iana","extensions":["tpl"]},"application/vnd.groove-vcard":{"source":"iana","extensions":["vcg"]},"application/vnd.hal+json":{"source":"iana","compressible":true},"application/vnd.hal+xml":{"source":"iana","compressible":true,"extensions":["hal"]},"application/vnd.handheld-entertainment+xml":{"source":"iana","compressible":true,"extensions":["zmm"]},"application/vnd.hbci":{"source":"iana","extensions":["hbci"]},"application/vnd.hc+json":{"source":"iana","compressible":true},"application/vnd.hcl-bireports":{"source":"iana"},"application/vnd.hdt":{"source":"iana"},"application/vnd.heroku+json":{"source":"iana","compressible":true},"application/vnd.hhe.lesson-player":{"source":"iana","extensions":["les"]},"application/vnd.hp-hpgl":{"source":"iana","extensions":["hpgl"]},"application/vnd.hp-hpid":{"source":"iana","extensions":["hpid"]},"application/vnd.hp-hps":{"source":"iana","extensions":["hps"]},"application/vnd.hp-jlyt":{"source":"iana","extensions":["jlt"]},"application/vnd.hp-pcl":{"source":"iana","extensions":["pcl"]},"application/vnd.hp-pclxl":{"source":"iana","extensions":["pclxl"]},"application/vnd.httphone":{"source":"iana"},"application/vnd.hydrostatix.sof-data":{"source":"iana","extensions":["sfd-hdstx"]},"application/vnd.hyper+json":{"source":"iana","compressible":true},"application/vnd.hyper-item+json":{"source":"iana","compressible":true},"application/vnd.hyperdrive+json":{"source":"iana","compressible":true},"application/vnd.hzn-3d-crossword":{"source":"iana"},"application/vnd.ibm.afplinedata":{"source":"iana"},"application/vnd.ibm.electronic-media":{"source":"iana"},"application/vnd.ibm.minipay":{"source":"iana","extensions":["mpy"]},"application/vnd.ibm.modcap":{"source":"iana","extensions":["afp","listafp","list3820"]},"application/vnd.ibm.rights-management":{"source":"iana","extensions":["irm"]},"application/vnd.ibm.secure-container":{"source":"iana","extensions":["sc"]},"application/vnd.iccprofile":{"source":"iana","extensions":["icc","icm"]},"application/vnd.ieee.1905":{"source":"iana"},"application/vnd.igloader":{"source":"iana","extensions":["igl"]},"application/vnd.imagemeter.folder+zip":{"source":"iana","compressible":false},"application/vnd.imagemeter.image+zip":{"source":"iana","compressible":false},"application/vnd.immervision-ivp":{"source":"iana","extensions":["ivp"]},"application/vnd.immervision-ivu":{"source":"iana","extensions":["ivu"]},"application/vnd.ims.imsccv1p1":{"source":"iana"},"application/vnd.ims.imsccv1p2":{"source":"iana"},"application/vnd.ims.imsccv1p3":{"source":"iana"},"application/vnd.ims.lis.v2.result+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolconsumerprofile+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolproxy+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolproxy.id+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolsettings+json":{"source":"iana","compressible":true},"application/vnd.ims.lti.v2.toolsettings.simple+json":{"source":"iana","compressible":true},"application/vnd.informedcontrol.rms+xml":{"source":"iana","compressible":true},"application/vnd.informix-visionary":{"source":"iana"},"application/vnd.infotech.project":{"source":"iana"},"application/vnd.infotech.project+xml":{"source":"iana","compressible":true},"application/vnd.innopath.wamp.notification":{"source":"iana"},"application/vnd.insors.igm":{"source":"iana","extensions":["igm"]},"application/vnd.intercon.formnet":{"source":"iana","extensions":["xpw","xpx"]},"application/vnd.intergeo":{"source":"iana","extensions":["i2g"]},"application/vnd.intertrust.digibox":{"source":"iana"},"application/vnd.intertrust.nncp":{"source":"iana"},"application/vnd.intu.qbo":{"source":"iana","extensions":["qbo"]},"application/vnd.intu.qfx":{"source":"iana","extensions":["qfx"]},"application/vnd.iptc.g2.catalogitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.conceptitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.knowledgeitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.newsitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.newsmessage+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.packageitem+xml":{"source":"iana","compressible":true},"application/vnd.iptc.g2.planningitem+xml":{"source":"iana","compressible":true},"application/vnd.ipunplugged.rcprofile":{"source":"iana","extensions":["rcprofile"]},"application/vnd.irepository.package+xml":{"source":"iana","compressible":true,"extensions":["irp"]},"application/vnd.is-xpr":{"source":"iana","extensions":["xpr"]},"application/vnd.isac.fcs":{"source":"iana","extensions":["fcs"]},"application/vnd.iso11783-10+zip":{"source":"iana","compressible":false},"application/vnd.jam":{"source":"iana","extensions":["jam"]},"application/vnd.japannet-directory-service":{"source":"iana"},"application/vnd.japannet-jpnstore-wakeup":{"source":"iana"},"application/vnd.japannet-payment-wakeup":{"source":"iana"},"application/vnd.japannet-registration":{"source":"iana"},"application/vnd.japannet-registration-wakeup":{"source":"iana"},"application/vnd.japannet-setstore-wakeup":{"source":"iana"},"application/vnd.japannet-verification":{"source":"iana"},"application/vnd.japannet-verification-wakeup":{"source":"iana"},"application/vnd.jcp.javame.midlet-rms":{"source":"iana","extensions":["rms"]},"application/vnd.jisp":{"source":"iana","extensions":["jisp"]},"application/vnd.joost.joda-archive":{"source":"iana","extensions":["joda"]},"application/vnd.jsk.isdn-ngn":{"source":"iana"},"application/vnd.kahootz":{"source":"iana","extensions":["ktz","ktr"]},"application/vnd.kde.karbon":{"source":"iana","extensions":["karbon"]},"application/vnd.kde.kchart":{"source":"iana","extensions":["chrt"]},"application/vnd.kde.kformula":{"source":"iana","extensions":["kfo"]},"application/vnd.kde.kivio":{"source":"iana","extensions":["flw"]},"application/vnd.kde.kontour":{"source":"iana","extensions":["kon"]},"application/vnd.kde.kpresenter":{"source":"iana","extensions":["kpr","kpt"]},"application/vnd.kde.kspread":{"source":"iana","extensions":["ksp"]},"application/vnd.kde.kword":{"source":"iana","extensions":["kwd","kwt"]},"application/vnd.kenameaapp":{"source":"iana","extensions":["htke"]},"application/vnd.kidspiration":{"source":"iana","extensions":["kia"]},"application/vnd.kinar":{"source":"iana","extensions":["kne","knp"]},"application/vnd.koan":{"source":"iana","extensions":["skp","skd","skt","skm"]},"application/vnd.kodak-descriptor":{"source":"iana","extensions":["sse"]},"application/vnd.las":{"source":"iana"},"application/vnd.las.las+json":{"source":"iana","compressible":true},"application/vnd.las.las+xml":{"source":"iana","compressible":true,"extensions":["lasxml"]},"application/vnd.laszip":{"source":"iana"},"application/vnd.leap+json":{"source":"iana","compressible":true},"application/vnd.liberty-request+xml":{"source":"iana","compressible":true},"application/vnd.llamagraphics.life-balance.desktop":{"source":"iana","extensions":["lbd"]},"application/vnd.llamagraphics.life-balance.exchange+xml":{"source":"iana","compressible":true,"extensions":["lbe"]},"application/vnd.logipipe.circuit+zip":{"source":"iana","compressible":false},"application/vnd.loom":{"source":"iana"},"application/vnd.lotus-1-2-3":{"source":"iana","extensions":["123"]},"application/vnd.lotus-approach":{"source":"iana","extensions":["apr"]},"application/vnd.lotus-freelance":{"source":"iana","extensions":["pre"]},"application/vnd.lotus-notes":{"source":"iana","extensions":["nsf"]},"application/vnd.lotus-organizer":{"source":"iana","extensions":["org"]},"application/vnd.lotus-screencam":{"source":"iana","extensions":["scm"]},"application/vnd.lotus-wordpro":{"source":"iana","extensions":["lwp"]},"application/vnd.macports.portpkg":{"source":"iana","extensions":["portpkg"]},"application/vnd.mapbox-vector-tile":{"source":"iana"},"application/vnd.marlin.drm.actiontoken+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.conftoken+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.license+xml":{"source":"iana","compressible":true},"application/vnd.marlin.drm.mdcf":{"source":"iana"},"application/vnd.mason+json":{"source":"iana","compressible":true},"application/vnd.maxmind.maxmind-db":{"source":"iana"},"application/vnd.mcd":{"source":"iana","extensions":["mcd"]},"application/vnd.medcalcdata":{"source":"iana","extensions":["mc1"]},"application/vnd.mediastation.cdkey":{"source":"iana","extensions":["cdkey"]},"application/vnd.meridian-slingshot":{"source":"iana"},"application/vnd.mfer":{"source":"iana","extensions":["mwf"]},"application/vnd.mfmp":{"source":"iana","extensions":["mfm"]},"application/vnd.micro+json":{"source":"iana","compressible":true},"application/vnd.micrografx.flo":{"source":"iana","extensions":["flo"]},"application/vnd.micrografx.igx":{"source":"iana","extensions":["igx"]},"application/vnd.microsoft.portable-executable":{"source":"iana"},"application/vnd.microsoft.windows.thumbnail-cache":{"source":"iana"},"application/vnd.miele+json":{"source":"iana","compressible":true},"application/vnd.mif":{"source":"iana","extensions":["mif"]},"application/vnd.minisoft-hp3000-save":{"source":"iana"},"application/vnd.mitsubishi.misty-guard.trustweb":{"source":"iana"},"application/vnd.mobius.daf":{"source":"iana","extensions":["daf"]},"application/vnd.mobius.dis":{"source":"iana","extensions":["dis"]},"application/vnd.mobius.mbk":{"source":"iana","extensions":["mbk"]},"application/vnd.mobius.mqy":{"source":"iana","extensions":["mqy"]},"application/vnd.mobius.msl":{"source":"iana","extensions":["msl"]},"application/vnd.mobius.plc":{"source":"iana","extensions":["plc"]},"application/vnd.mobius.txf":{"source":"iana","extensions":["txf"]},"application/vnd.mophun.application":{"source":"iana","extensions":["mpn"]},"application/vnd.mophun.certificate":{"source":"iana","extensions":["mpc"]},"application/vnd.motorola.flexsuite":{"source":"iana"},"application/vnd.motorola.flexsuite.adsi":{"source":"iana"},"application/vnd.motorola.flexsuite.fis":{"source":"iana"},"application/vnd.motorola.flexsuite.gotap":{"source":"iana"},"application/vnd.motorola.flexsuite.kmr":{"source":"iana"},"application/vnd.motorola.flexsuite.ttc":{"source":"iana"},"application/vnd.motorola.flexsuite.wem":{"source":"iana"},"application/vnd.motorola.iprm":{"source":"iana"},"application/vnd.mozilla.xul+xml":{"source":"iana","compressible":true,"extensions":["xul"]},"application/vnd.ms-3mfdocument":{"source":"iana"},"application/vnd.ms-artgalry":{"source":"iana","extensions":["cil"]},"application/vnd.ms-asf":{"source":"iana"},"application/vnd.ms-cab-compressed":{"source":"iana","extensions":["cab"]},"application/vnd.ms-color.iccprofile":{"source":"apache"},"application/vnd.ms-excel":{"source":"iana","compressible":false,"extensions":["xls","xlm","xla","xlc","xlt","xlw"]},"application/vnd.ms-excel.addin.macroenabled.12":{"source":"iana","extensions":["xlam"]},"application/vnd.ms-excel.sheet.binary.macroenabled.12":{"source":"iana","extensions":["xlsb"]},"application/vnd.ms-excel.sheet.macroenabled.12":{"source":"iana","extensions":["xlsm"]},"application/vnd.ms-excel.template.macroenabled.12":{"source":"iana","extensions":["xltm"]},"application/vnd.ms-fontobject":{"source":"iana","compressible":true,"extensions":["eot"]},"application/vnd.ms-htmlhelp":{"source":"iana","extensions":["chm"]},"application/vnd.ms-ims":{"source":"iana","extensions":["ims"]},"application/vnd.ms-lrm":{"source":"iana","extensions":["lrm"]},"application/vnd.ms-office.activex+xml":{"source":"iana","compressible":true},"application/vnd.ms-officetheme":{"source":"iana","extensions":["thmx"]},"application/vnd.ms-opentype":{"source":"apache","compressible":true},"application/vnd.ms-outlook":{"compressible":false,"extensions":["msg"]},"application/vnd.ms-package.obfuscated-opentype":{"source":"apache"},"application/vnd.ms-pki.seccat":{"source":"apache","extensions":["cat"]},"application/vnd.ms-pki.stl":{"source":"apache","extensions":["stl"]},"application/vnd.ms-playready.initiator+xml":{"source":"iana","compressible":true},"application/vnd.ms-powerpoint":{"source":"iana","compressible":false,"extensions":["ppt","pps","pot"]},"application/vnd.ms-powerpoint.addin.macroenabled.12":{"source":"iana","extensions":["ppam"]},"application/vnd.ms-powerpoint.presentation.macroenabled.12":{"source":"iana","extensions":["pptm"]},"application/vnd.ms-powerpoint.slide.macroenabled.12":{"source":"iana","extensions":["sldm"]},"application/vnd.ms-powerpoint.slideshow.macroenabled.12":{"source":"iana","extensions":["ppsm"]},"application/vnd.ms-powerpoint.template.macroenabled.12":{"source":"iana","extensions":["potm"]},"application/vnd.ms-printdevicecapabilities+xml":{"source":"iana","compressible":true},"application/vnd.ms-printing.printticket+xml":{"source":"apache","compressible":true},"application/vnd.ms-printschematicket+xml":{"source":"iana","compressible":true},"application/vnd.ms-project":{"source":"iana","extensions":["mpp","mpt"]},"application/vnd.ms-tnef":{"source":"iana"},"application/vnd.ms-windows.devicepairing":{"source":"iana"},"application/vnd.ms-windows.nwprinting.oob":{"source":"iana"},"application/vnd.ms-windows.printerpairing":{"source":"iana"},"application/vnd.ms-windows.wsd.oob":{"source":"iana"},"application/vnd.ms-wmdrm.lic-chlg-req":{"source":"iana"},"application/vnd.ms-wmdrm.lic-resp":{"source":"iana"},"application/vnd.ms-wmdrm.meter-chlg-req":{"source":"iana"},"application/vnd.ms-wmdrm.meter-resp":{"source":"iana"},"application/vnd.ms-word.document.macroenabled.12":{"source":"iana","extensions":["docm"]},"application/vnd.ms-word.template.macroenabled.12":{"source":"iana","extensions":["dotm"]},"application/vnd.ms-works":{"source":"iana","extensions":["wps","wks","wcm","wdb"]},"application/vnd.ms-wpl":{"source":"iana","extensions":["wpl"]},"application/vnd.ms-xpsdocument":{"source":"iana","compressible":false,"extensions":["xps"]},"application/vnd.msa-disk-image":{"source":"iana"},"application/vnd.mseq":{"source":"iana","extensions":["mseq"]},"application/vnd.msign":{"source":"iana"},"application/vnd.multiad.creator":{"source":"iana"},"application/vnd.multiad.creator.cif":{"source":"iana"},"application/vnd.music-niff":{"source":"iana"},"application/vnd.musician":{"source":"iana","extensions":["mus"]},"application/vnd.muvee.style":{"source":"iana","extensions":["msty"]},"application/vnd.mynfc":{"source":"iana","extensions":["taglet"]},"application/vnd.ncd.control":{"source":"iana"},"application/vnd.ncd.reference":{"source":"iana"},"application/vnd.nearst.inv+json":{"source":"iana","compressible":true},"application/vnd.nervana":{"source":"iana"},"application/vnd.netfpx":{"source":"iana"},"application/vnd.neurolanguage.nlu":{"source":"iana","extensions":["nlu"]},"application/vnd.nimn":{"source":"iana"},"application/vnd.nintendo.nitro.rom":{"source":"iana"},"application/vnd.nintendo.snes.rom":{"source":"iana"},"application/vnd.nitf":{"source":"iana","extensions":["ntf","nitf"]},"application/vnd.noblenet-directory":{"source":"iana","extensions":["nnd"]},"application/vnd.noblenet-sealer":{"source":"iana","extensions":["nns"]},"application/vnd.noblenet-web":{"source":"iana","extensions":["nnw"]},"application/vnd.nokia.catalogs":{"source":"iana"},"application/vnd.nokia.conml+wbxml":{"source":"iana"},"application/vnd.nokia.conml+xml":{"source":"iana","compressible":true},"application/vnd.nokia.iptv.config+xml":{"source":"iana","compressible":true},"application/vnd.nokia.isds-radio-presets":{"source":"iana"},"application/vnd.nokia.landmark+wbxml":{"source":"iana"},"application/vnd.nokia.landmark+xml":{"source":"iana","compressible":true},"application/vnd.nokia.landmarkcollection+xml":{"source":"iana","compressible":true},"application/vnd.nokia.n-gage.ac+xml":{"source":"iana","compressible":true,"extensions":["ac"]},"application/vnd.nokia.n-gage.data":{"source":"iana","extensions":["ngdat"]},"application/vnd.nokia.n-gage.symbian.install":{"source":"iana","extensions":["n-gage"]},"application/vnd.nokia.ncd":{"source":"iana"},"application/vnd.nokia.pcd+wbxml":{"source":"iana"},"application/vnd.nokia.pcd+xml":{"source":"iana","compressible":true},"application/vnd.nokia.radio-preset":{"source":"iana","extensions":["rpst"]},"application/vnd.nokia.radio-presets":{"source":"iana","extensions":["rpss"]},"application/vnd.novadigm.edm":{"source":"iana","extensions":["edm"]},"application/vnd.novadigm.edx":{"source":"iana","extensions":["edx"]},"application/vnd.novadigm.ext":{"source":"iana","extensions":["ext"]},"application/vnd.ntt-local.content-share":{"source":"iana"},"application/vnd.ntt-local.file-transfer":{"source":"iana"},"application/vnd.ntt-local.ogw_remote-access":{"source":"iana"},"application/vnd.ntt-local.sip-ta_remote":{"source":"iana"},"application/vnd.ntt-local.sip-ta_tcp_stream":{"source":"iana"},"application/vnd.oasis.opendocument.chart":{"source":"iana","extensions":["odc"]},"application/vnd.oasis.opendocument.chart-template":{"source":"iana","extensions":["otc"]},"application/vnd.oasis.opendocument.database":{"source":"iana","extensions":["odb"]},"application/vnd.oasis.opendocument.formula":{"source":"iana","extensions":["odf"]},"application/vnd.oasis.opendocument.formula-template":{"source":"iana","extensions":["odft"]},"application/vnd.oasis.opendocument.graphics":{"source":"iana","compressible":false,"extensions":["odg"]},"application/vnd.oasis.opendocument.graphics-template":{"source":"iana","extensions":["otg"]},"application/vnd.oasis.opendocument.image":{"source":"iana","extensions":["odi"]},"application/vnd.oasis.opendocument.image-template":{"source":"iana","extensions":["oti"]},"application/vnd.oasis.opendocument.presentation":{"source":"iana","compressible":false,"extensions":["odp"]},"application/vnd.oasis.opendocument.presentation-template":{"source":"iana","extensions":["otp"]},"application/vnd.oasis.opendocument.spreadsheet":{"source":"iana","compressible":false,"extensions":["ods"]},"application/vnd.oasis.opendocument.spreadsheet-template":{"source":"iana","extensions":["ots"]},"application/vnd.oasis.opendocument.text":{"source":"iana","compressible":false,"extensions":["odt"]},"application/vnd.oasis.opendocument.text-master":{"source":"iana","extensions":["odm"]},"application/vnd.oasis.opendocument.text-template":{"source":"iana","extensions":["ott"]},"application/vnd.oasis.opendocument.text-web":{"source":"iana","extensions":["oth"]},"application/vnd.obn":{"source":"iana"},"application/vnd.ocf+cbor":{"source":"iana"},"application/vnd.oci.image.manifest.v1+json":{"source":"iana","compressible":true},"application/vnd.oftn.l10n+json":{"source":"iana","compressible":true},"application/vnd.oipf.contentaccessdownload+xml":{"source":"iana","compressible":true},"application/vnd.oipf.contentaccessstreaming+xml":{"source":"iana","compressible":true},"application/vnd.oipf.cspg-hexbinary":{"source":"iana"},"application/vnd.oipf.dae.svg+xml":{"source":"iana","compressible":true},"application/vnd.oipf.dae.xhtml+xml":{"source":"iana","compressible":true},"application/vnd.oipf.mippvcontrolmessage+xml":{"source":"iana","compressible":true},"application/vnd.oipf.pae.gem":{"source":"iana"},"application/vnd.oipf.spdiscovery+xml":{"source":"iana","compressible":true},"application/vnd.oipf.spdlist+xml":{"source":"iana","compressible":true},"application/vnd.oipf.ueprofile+xml":{"source":"iana","compressible":true},"application/vnd.oipf.userprofile+xml":{"source":"iana","compressible":true},"application/vnd.olpc-sugar":{"source":"iana","extensions":["xo"]},"application/vnd.oma-scws-config":{"source":"iana"},"application/vnd.oma-scws-http-request":{"source":"iana"},"application/vnd.oma-scws-http-response":{"source":"iana"},"application/vnd.oma.bcast.associated-procedure-parameter+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.drm-trigger+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.imd+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.ltkm":{"source":"iana"},"application/vnd.oma.bcast.notification+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.provisioningtrigger":{"source":"iana"},"application/vnd.oma.bcast.sgboot":{"source":"iana"},"application/vnd.oma.bcast.sgdd+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.sgdu":{"source":"iana"},"application/vnd.oma.bcast.simple-symbol-container":{"source":"iana"},"application/vnd.oma.bcast.smartcard-trigger+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.sprov+xml":{"source":"iana","compressible":true},"application/vnd.oma.bcast.stkm":{"source":"iana"},"application/vnd.oma.cab-address-book+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-feature-handler+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-pcc+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-subs-invite+xml":{"source":"iana","compressible":true},"application/vnd.oma.cab-user-prefs+xml":{"source":"iana","compressible":true},"application/vnd.oma.dcd":{"source":"iana"},"application/vnd.oma.dcdc":{"source":"iana"},"application/vnd.oma.dd2+xml":{"source":"iana","compressible":true,"extensions":["dd2"]},"application/vnd.oma.drm.risd+xml":{"source":"iana","compressible":true},"application/vnd.oma.group-usage-list+xml":{"source":"iana","compressible":true},"application/vnd.oma.lwm2m+json":{"source":"iana","compressible":true},"application/vnd.oma.lwm2m+tlv":{"source":"iana"},"application/vnd.oma.pal+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.detailed-progress-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.final-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.groups+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.invocation-descriptor+xml":{"source":"iana","compressible":true},"application/vnd.oma.poc.optimized-progress-report+xml":{"source":"iana","compressible":true},"application/vnd.oma.push":{"source":"iana"},"application/vnd.oma.scidm.messages+xml":{"source":"iana","compressible":true},"application/vnd.oma.xcap-directory+xml":{"source":"iana","compressible":true},"application/vnd.omads-email+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omads-file+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omads-folder+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.omaloc-supl-init":{"source":"iana"},"application/vnd.onepager":{"source":"iana"},"application/vnd.onepagertamp":{"source":"iana"},"application/vnd.onepagertamx":{"source":"iana"},"application/vnd.onepagertat":{"source":"iana"},"application/vnd.onepagertatp":{"source":"iana"},"application/vnd.onepagertatx":{"source":"iana"},"application/vnd.openblox.game+xml":{"source":"iana","compressible":true,"extensions":["obgx"]},"application/vnd.openblox.game-binary":{"source":"iana"},"application/vnd.openeye.oeb":{"source":"iana"},"application/vnd.openofficeorg.extension":{"source":"apache","extensions":["oxt"]},"application/vnd.openstreetmap.data+xml":{"source":"iana","compressible":true,"extensions":["osm"]},"application/vnd.openxmlformats-officedocument.custom-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.customxmlproperties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawing+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.chart+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramcolors+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramdata+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramlayout+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.drawingml.diagramstyle+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.extended-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.commentauthors+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.handoutmaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.notesmaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.notesslide+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.presentation":{"source":"iana","compressible":false,"extensions":["pptx"]},"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.presprops+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slide":{"source":"iana","extensions":["sldx"]},"application/vnd.openxmlformats-officedocument.presentationml.slide+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slidelayout+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slidemaster+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slideshow":{"source":"iana","extensions":["ppsx"]},"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.slideupdateinfo+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.tablestyles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.tags+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.template":{"source":"iana","extensions":["potx"]},"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.presentationml.viewprops+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.calcchain+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.externallink+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcachedefinition+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotcacherecords+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.pivottable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.querytable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionheaders+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionlog+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedstrings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":{"source":"iana","compressible":false,"extensions":["xlsx"]},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetmetadata+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.tablesinglecells+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.template":{"source":"iana","extensions":["xltx"]},"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.usernames+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.volatiledependencies+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.theme+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.themeoverride+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.vmldrawing":{"source":"iana"},"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.document":{"source":"iana","compressible":false,"extensions":["docx"]},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.fonttable+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.template":{"source":"iana","extensions":["dotx"]},"application/vnd.openxmlformats-officedocument.wordprocessingml.template.main+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-officedocument.wordprocessingml.websettings+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.core-properties+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml":{"source":"iana","compressible":true},"application/vnd.openxmlformats-package.relationships+xml":{"source":"iana","compressible":true},"application/vnd.oracle.resource+json":{"source":"iana","compressible":true},"application/vnd.orange.indata":{"source":"iana"},"application/vnd.osa.netdeploy":{"source":"iana"},"application/vnd.osgeo.mapguide.package":{"source":"iana","extensions":["mgp"]},"application/vnd.osgi.bundle":{"source":"iana"},"application/vnd.osgi.dp":{"source":"iana","extensions":["dp"]},"application/vnd.osgi.subsystem":{"source":"iana","extensions":["esa"]},"application/vnd.otps.ct-kip+xml":{"source":"iana","compressible":true},"application/vnd.oxli.countgraph":{"source":"iana"},"application/vnd.pagerduty+json":{"source":"iana","compressible":true},"application/vnd.palm":{"source":"iana","extensions":["pdb","pqa","oprc"]},"application/vnd.panoply":{"source":"iana"},"application/vnd.paos.xml":{"source":"iana"},"application/vnd.patentdive":{"source":"iana"},"application/vnd.patientecommsdoc":{"source":"iana"},"application/vnd.pawaafile":{"source":"iana","extensions":["paw"]},"application/vnd.pcos":{"source":"iana"},"application/vnd.pg.format":{"source":"iana","extensions":["str"]},"application/vnd.pg.osasli":{"source":"iana","extensions":["ei6"]},"application/vnd.piaccess.application-licence":{"source":"iana"},"application/vnd.picsel":{"source":"iana","extensions":["efif"]},"application/vnd.pmi.widget":{"source":"iana","extensions":["wg"]},"application/vnd.poc.group-advertisement+xml":{"source":"iana","compressible":true},"application/vnd.pocketlearn":{"source":"iana","extensions":["plf"]},"application/vnd.powerbuilder6":{"source":"iana","extensions":["pbd"]},"application/vnd.powerbuilder6-s":{"source":"iana"},"application/vnd.powerbuilder7":{"source":"iana"},"application/vnd.powerbuilder7-s":{"source":"iana"},"application/vnd.powerbuilder75":{"source":"iana"},"application/vnd.powerbuilder75-s":{"source":"iana"},"application/vnd.preminet":{"source":"iana"},"application/vnd.previewsystems.box":{"source":"iana","extensions":["box"]},"application/vnd.proteus.magazine":{"source":"iana","extensions":["mgz"]},"application/vnd.psfs":{"source":"iana"},"application/vnd.publishare-delta-tree":{"source":"iana","extensions":["qps"]},"application/vnd.pvi.ptid1":{"source":"iana","extensions":["ptid"]},"application/vnd.pwg-multiplexed":{"source":"iana"},"application/vnd.pwg-xhtml-print+xml":{"source":"iana","compressible":true},"application/vnd.qualcomm.brew-app-res":{"source":"iana"},"application/vnd.quarantainenet":{"source":"iana"},"application/vnd.quark.quarkxpress":{"source":"iana","extensions":["qxd","qxt","qwd","qwt","qxl","qxb"]},"application/vnd.quobject-quoxdocument":{"source":"iana"},"application/vnd.radisys.moml+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-conf+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-conn+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-dialog+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-audit-stream+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-conf+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-base+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-fax-detect+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-fax-sendrecv+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-group+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-speech+xml":{"source":"iana","compressible":true},"application/vnd.radisys.msml-dialog-transform+xml":{"source":"iana","compressible":true},"application/vnd.rainstor.data":{"source":"iana"},"application/vnd.rapid":{"source":"iana"},"application/vnd.rar":{"source":"iana"},"application/vnd.realvnc.bed":{"source":"iana","extensions":["bed"]},"application/vnd.recordare.musicxml":{"source":"iana","extensions":["mxl"]},"application/vnd.recordare.musicxml+xml":{"source":"iana","compressible":true,"extensions":["musicxml"]},"application/vnd.renlearn.rlprint":{"source":"iana"},"application/vnd.restful+json":{"source":"iana","compressible":true},"application/vnd.rig.cryptonote":{"source":"iana","extensions":["cryptonote"]},"application/vnd.rim.cod":{"source":"apache","extensions":["cod"]},"application/vnd.rn-realmedia":{"source":"apache","extensions":["rm"]},"application/vnd.rn-realmedia-vbr":{"source":"apache","extensions":["rmvb"]},"application/vnd.route66.link66+xml":{"source":"iana","compressible":true,"extensions":["link66"]},"application/vnd.rs-274x":{"source":"iana"},"application/vnd.ruckus.download":{"source":"iana"},"application/vnd.s3sms":{"source":"iana"},"application/vnd.sailingtracker.track":{"source":"iana","extensions":["st"]},"application/vnd.sar":{"source":"iana"},"application/vnd.sbm.cid":{"source":"iana"},"application/vnd.sbm.mid2":{"source":"iana"},"application/vnd.scribus":{"source":"iana"},"application/vnd.sealed.3df":{"source":"iana"},"application/vnd.sealed.csf":{"source":"iana"},"application/vnd.sealed.doc":{"source":"iana"},"application/vnd.sealed.eml":{"source":"iana"},"application/vnd.sealed.mht":{"source":"iana"},"application/vnd.sealed.net":{"source":"iana"},"application/vnd.sealed.ppt":{"source":"iana"},"application/vnd.sealed.tiff":{"source":"iana"},"application/vnd.sealed.xls":{"source":"iana"},"application/vnd.sealedmedia.softseal.html":{"source":"iana"},"application/vnd.sealedmedia.softseal.pdf":{"source":"iana"},"application/vnd.seemail":{"source":"iana","extensions":["see"]},"application/vnd.sema":{"source":"iana","extensions":["sema"]},"application/vnd.semd":{"source":"iana","extensions":["semd"]},"application/vnd.semf":{"source":"iana","extensions":["semf"]},"application/vnd.shade-save-file":{"source":"iana"},"application/vnd.shana.informed.formdata":{"source":"iana","extensions":["ifm"]},"application/vnd.shana.informed.formtemplate":{"source":"iana","extensions":["itp"]},"application/vnd.shana.informed.interchange":{"source":"iana","extensions":["iif"]},"application/vnd.shana.informed.package":{"source":"iana","extensions":["ipk"]},"application/vnd.shootproof+json":{"source":"iana","compressible":true},"application/vnd.shopkick+json":{"source":"iana","compressible":true},"application/vnd.shp":{"source":"iana"},"application/vnd.shx":{"source":"iana"},"application/vnd.sigrok.session":{"source":"iana"},"application/vnd.simtech-mindmapper":{"source":"iana","extensions":["twd","twds"]},"application/vnd.siren+json":{"source":"iana","compressible":true},"application/vnd.smaf":{"source":"iana","extensions":["mmf"]},"application/vnd.smart.notebook":{"source":"iana"},"application/vnd.smart.teacher":{"source":"iana","extensions":["teacher"]},"application/vnd.snesdev-page-table":{"source":"iana"},"application/vnd.software602.filler.form+xml":{"source":"iana","compressible":true,"extensions":["fo"]},"application/vnd.software602.filler.form-xml-zip":{"source":"iana"},"application/vnd.solent.sdkm+xml":{"source":"iana","compressible":true,"extensions":["sdkm","sdkd"]},"application/vnd.spotfire.dxp":{"source":"iana","extensions":["dxp"]},"application/vnd.spotfire.sfs":{"source":"iana","extensions":["sfs"]},"application/vnd.sqlite3":{"source":"iana"},"application/vnd.sss-cod":{"source":"iana"},"application/vnd.sss-dtf":{"source":"iana"},"application/vnd.sss-ntf":{"source":"iana"},"application/vnd.stardivision.calc":{"source":"apache","extensions":["sdc"]},"application/vnd.stardivision.draw":{"source":"apache","extensions":["sda"]},"application/vnd.stardivision.impress":{"source":"apache","extensions":["sdd"]},"application/vnd.stardivision.math":{"source":"apache","extensions":["smf"]},"application/vnd.stardivision.writer":{"source":"apache","extensions":["sdw","vor"]},"application/vnd.stardivision.writer-global":{"source":"apache","extensions":["sgl"]},"application/vnd.stepmania.package":{"source":"iana","extensions":["smzip"]},"application/vnd.stepmania.stepchart":{"source":"iana","extensions":["sm"]},"application/vnd.street-stream":{"source":"iana"},"application/vnd.sun.wadl+xml":{"source":"iana","compressible":true,"extensions":["wadl"]},"application/vnd.sun.xml.calc":{"source":"apache","extensions":["sxc"]},"application/vnd.sun.xml.calc.template":{"source":"apache","extensions":["stc"]},"application/vnd.sun.xml.draw":{"source":"apache","extensions":["sxd"]},"application/vnd.sun.xml.draw.template":{"source":"apache","extensions":["std"]},"application/vnd.sun.xml.impress":{"source":"apache","extensions":["sxi"]},"application/vnd.sun.xml.impress.template":{"source":"apache","extensions":["sti"]},"application/vnd.sun.xml.math":{"source":"apache","extensions":["sxm"]},"application/vnd.sun.xml.writer":{"source":"apache","extensions":["sxw"]},"application/vnd.sun.xml.writer.global":{"source":"apache","extensions":["sxg"]},"application/vnd.sun.xml.writer.template":{"source":"apache","extensions":["stw"]},"application/vnd.sus-calendar":{"source":"iana","extensions":["sus","susp"]},"application/vnd.svd":{"source":"iana","extensions":["svd"]},"application/vnd.swiftview-ics":{"source":"iana"},"application/vnd.symbian.install":{"source":"apache","extensions":["sis","sisx"]},"application/vnd.syncml+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["xsm"]},"application/vnd.syncml.dm+wbxml":{"source":"iana","charset":"UTF-8","extensions":["bdm"]},"application/vnd.syncml.dm+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["xdm"]},"application/vnd.syncml.dm.notification":{"source":"iana"},"application/vnd.syncml.dmddf+wbxml":{"source":"iana"},"application/vnd.syncml.dmddf+xml":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["ddf"]},"application/vnd.syncml.dmtnds+wbxml":{"source":"iana"},"application/vnd.syncml.dmtnds+xml":{"source":"iana","charset":"UTF-8","compressible":true},"application/vnd.syncml.ds.notification":{"source":"iana"},"application/vnd.tableschema+json":{"source":"iana","compressible":true},"application/vnd.tao.intent-module-archive":{"source":"iana","extensions":["tao"]},"application/vnd.tcpdump.pcap":{"source":"iana","extensions":["pcap","cap","dmp"]},"application/vnd.think-cell.ppttc+json":{"source":"iana","compressible":true},"application/vnd.tmd.mediaflex.api+xml":{"source":"iana","compressible":true},"application/vnd.tml":{"source":"iana"},"application/vnd.tmobile-livetv":{"source":"iana","extensions":["tmo"]},"application/vnd.tri.onesource":{"source":"iana"},"application/vnd.trid.tpt":{"source":"iana","extensions":["tpt"]},"application/vnd.triscape.mxs":{"source":"iana","extensions":["mxs"]},"application/vnd.trueapp":{"source":"iana","extensions":["tra"]},"application/vnd.truedoc":{"source":"iana"},"application/vnd.ubisoft.webplayer":{"source":"iana"},"application/vnd.ufdl":{"source":"iana","extensions":["ufd","ufdl"]},"application/vnd.uiq.theme":{"source":"iana","extensions":["utz"]},"application/vnd.umajin":{"source":"iana","extensions":["umj"]},"application/vnd.unity":{"source":"iana","extensions":["unityweb"]},"application/vnd.uoml+xml":{"source":"iana","compressible":true,"extensions":["uoml"]},"application/vnd.uplanet.alert":{"source":"iana"},"application/vnd.uplanet.alert-wbxml":{"source":"iana"},"application/vnd.uplanet.bearer-choice":{"source":"iana"},"application/vnd.uplanet.bearer-choice-wbxml":{"source":"iana"},"application/vnd.uplanet.cacheop":{"source":"iana"},"application/vnd.uplanet.cacheop-wbxml":{"source":"iana"},"application/vnd.uplanet.channel":{"source":"iana"},"application/vnd.uplanet.channel-wbxml":{"source":"iana"},"application/vnd.uplanet.list":{"source":"iana"},"application/vnd.uplanet.list-wbxml":{"source":"iana"},"application/vnd.uplanet.listcmd":{"source":"iana"},"application/vnd.uplanet.listcmd-wbxml":{"source":"iana"},"application/vnd.uplanet.signal":{"source":"iana"},"application/vnd.uri-map":{"source":"iana"},"application/vnd.valve.source.material":{"source":"iana"},"application/vnd.vcx":{"source":"iana","extensions":["vcx"]},"application/vnd.vd-study":{"source":"iana"},"application/vnd.vectorworks":{"source":"iana"},"application/vnd.vel+json":{"source":"iana","compressible":true},"application/vnd.verimatrix.vcas":{"source":"iana"},"application/vnd.veryant.thin":{"source":"iana"},"application/vnd.ves.encrypted":{"source":"iana"},"application/vnd.vidsoft.vidconference":{"source":"iana"},"application/vnd.visio":{"source":"iana","extensions":["vsd","vst","vss","vsw"]},"application/vnd.visionary":{"source":"iana","extensions":["vis"]},"application/vnd.vividence.scriptfile":{"source":"iana"},"application/vnd.vsf":{"source":"iana","extensions":["vsf"]},"application/vnd.wap.sic":{"source":"iana"},"application/vnd.wap.slc":{"source":"iana"},"application/vnd.wap.wbxml":{"source":"iana","charset":"UTF-8","extensions":["wbxml"]},"application/vnd.wap.wmlc":{"source":"iana","extensions":["wmlc"]},"application/vnd.wap.wmlscriptc":{"source":"iana","extensions":["wmlsc"]},"application/vnd.webturbo":{"source":"iana","extensions":["wtb"]},"application/vnd.wfa.p2p":{"source":"iana"},"application/vnd.wfa.wsc":{"source":"iana"},"application/vnd.windows.devicepairing":{"source":"iana"},"application/vnd.wmc":{"source":"iana"},"application/vnd.wmf.bootstrap":{"source":"iana"},"application/vnd.wolfram.mathematica":{"source":"iana"},"application/vnd.wolfram.mathematica.package":{"source":"iana"},"application/vnd.wolfram.player":{"source":"iana","extensions":["nbp"]},"application/vnd.wordperfect":{"source":"iana","extensions":["wpd"]},"application/vnd.wqd":{"source":"iana","extensions":["wqd"]},"application/vnd.wrq-hp3000-labelled":{"source":"iana"},"application/vnd.wt.stf":{"source":"iana","extensions":["stf"]},"application/vnd.wv.csp+wbxml":{"source":"iana"},"application/vnd.wv.csp+xml":{"source":"iana","compressible":true},"application/vnd.wv.ssp+xml":{"source":"iana","compressible":true},"application/vnd.xacml+json":{"source":"iana","compressible":true},"application/vnd.xara":{"source":"iana","extensions":["xar"]},"application/vnd.xfdl":{"source":"iana","extensions":["xfdl"]},"application/vnd.xfdl.webform":{"source":"iana"},"application/vnd.xmi+xml":{"source":"iana","compressible":true},"application/vnd.xmpie.cpkg":{"source":"iana"},"application/vnd.xmpie.dpkg":{"source":"iana"},"application/vnd.xmpie.plan":{"source":"iana"},"application/vnd.xmpie.ppkg":{"source":"iana"},"application/vnd.xmpie.xlim":{"source":"iana"},"application/vnd.yamaha.hv-dic":{"source":"iana","extensions":["hvd"]},"application/vnd.yamaha.hv-script":{"source":"iana","extensions":["hvs"]},"application/vnd.yamaha.hv-voice":{"source":"iana","extensions":["hvp"]},"application/vnd.yamaha.openscoreformat":{"source":"iana","extensions":["osf"]},"application/vnd.yamaha.openscoreformat.osfpvg+xml":{"source":"iana","compressible":true,"extensions":["osfpvg"]},"application/vnd.yamaha.remote-setup":{"source":"iana"},"application/vnd.yamaha.smaf-audio":{"source":"iana","extensions":["saf"]},"application/vnd.yamaha.smaf-phrase":{"source":"iana","extensions":["spf"]},"application/vnd.yamaha.through-ngn":{"source":"iana"},"application/vnd.yamaha.tunnel-udpencap":{"source":"iana"},"application/vnd.yaoweme":{"source":"iana"},"application/vnd.yellowriver-custom-menu":{"source":"iana","extensions":["cmp"]},"application/vnd.youtube.yt":{"source":"iana"},"application/vnd.zul":{"source":"iana","extensions":["zir","zirz"]},"application/vnd.zzazz.deck+xml":{"source":"iana","compressible":true,"extensions":["zaz"]},"application/voicexml+xml":{"source":"iana","compressible":true,"extensions":["vxml"]},"application/voucher-cms+json":{"source":"iana","compressible":true},"application/vq-rtcpxr":{"source":"iana"},"application/wasm":{"compressible":true,"extensions":["wasm"]},"application/watcherinfo+xml":{"source":"iana","compressible":true},"application/webpush-options+json":{"source":"iana","compressible":true},"application/whoispp-query":{"source":"iana"},"application/whoispp-response":{"source":"iana"},"application/widget":{"source":"iana","extensions":["wgt"]},"application/winhlp":{"source":"apache","extensions":["hlp"]},"application/wita":{"source":"iana"},"application/wordperfect5.1":{"source":"iana"},"application/wsdl+xml":{"source":"iana","compressible":true,"extensions":["wsdl"]},"application/wspolicy+xml":{"source":"iana","compressible":true,"extensions":["wspolicy"]},"application/x-7z-compressed":{"source":"apache","compressible":false,"extensions":["7z"]},"application/x-abiword":{"source":"apache","extensions":["abw"]},"application/x-ace-compressed":{"source":"apache","extensions":["ace"]},"application/x-amf":{"source":"apache"},"application/x-apple-diskimage":{"source":"apache","extensions":["dmg"]},"application/x-arj":{"compressible":false,"extensions":["arj"]},"application/x-authorware-bin":{"source":"apache","extensions":["aab","x32","u32","vox"]},"application/x-authorware-map":{"source":"apache","extensions":["aam"]},"application/x-authorware-seg":{"source":"apache","extensions":["aas"]},"application/x-bcpio":{"source":"apache","extensions":["bcpio"]},"application/x-bdoc":{"compressible":false,"extensions":["bdoc"]},"application/x-bittorrent":{"source":"apache","extensions":["torrent"]},"application/x-blorb":{"source":"apache","extensions":["blb","blorb"]},"application/x-bzip":{"source":"apache","compressible":false,"extensions":["bz"]},"application/x-bzip2":{"source":"apache","compressible":false,"extensions":["bz2","boz"]},"application/x-cbr":{"source":"apache","extensions":["cbr","cba","cbt","cbz","cb7"]},"application/x-cdlink":{"source":"apache","extensions":["vcd"]},"application/x-cfs-compressed":{"source":"apache","extensions":["cfs"]},"application/x-chat":{"source":"apache","extensions":["chat"]},"application/x-chess-pgn":{"source":"apache","extensions":["pgn"]},"application/x-chrome-extension":{"extensions":["crx"]},"application/x-cocoa":{"source":"nginx","extensions":["cco"]},"application/x-compress":{"source":"apache"},"application/x-conference":{"source":"apache","extensions":["nsc"]},"application/x-cpio":{"source":"apache","extensions":["cpio"]},"application/x-csh":{"source":"apache","extensions":["csh"]},"application/x-deb":{"compressible":false},"application/x-debian-package":{"source":"apache","extensions":["deb","udeb"]},"application/x-dgc-compressed":{"source":"apache","extensions":["dgc"]},"application/x-director":{"source":"apache","extensions":["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"]},"application/x-doom":{"source":"apache","extensions":["wad"]},"application/x-dtbncx+xml":{"source":"apache","compressible":true,"extensions":["ncx"]},"application/x-dtbook+xml":{"source":"apache","compressible":true,"extensions":["dtb"]},"application/x-dtbresource+xml":{"source":"apache","compressible":true,"extensions":["res"]},"application/x-dvi":{"source":"apache","compressible":false,"extensions":["dvi"]},"application/x-envoy":{"source":"apache","extensions":["evy"]},"application/x-eva":{"source":"apache","extensions":["eva"]},"application/x-font-bdf":{"source":"apache","extensions":["bdf"]},"application/x-font-dos":{"source":"apache"},"application/x-font-framemaker":{"source":"apache"},"application/x-font-ghostscript":{"source":"apache","extensions":["gsf"]},"application/x-font-libgrx":{"source":"apache"},"application/x-font-linux-psf":{"source":"apache","extensions":["psf"]},"application/x-font-pcf":{"source":"apache","extensions":["pcf"]},"application/x-font-snf":{"source":"apache","extensions":["snf"]},"application/x-font-speedo":{"source":"apache"},"application/x-font-sunos-news":{"source":"apache"},"application/x-font-type1":{"source":"apache","extensions":["pfa","pfb","pfm","afm"]},"application/x-font-vfont":{"source":"apache"},"application/x-freearc":{"source":"apache","extensions":["arc"]},"application/x-futuresplash":{"source":"apache","extensions":["spl"]},"application/x-gca-compressed":{"source":"apache","extensions":["gca"]},"application/x-glulx":{"source":"apache","extensions":["ulx"]},"application/x-gnumeric":{"source":"apache","extensions":["gnumeric"]},"application/x-gramps-xml":{"source":"apache","extensions":["gramps"]},"application/x-gtar":{"source":"apache","extensions":["gtar"]},"application/x-gzip":{"source":"apache"},"application/x-hdf":{"source":"apache","extensions":["hdf"]},"application/x-httpd-php":{"compressible":true,"extensions":["php"]},"application/x-install-instructions":{"source":"apache","extensions":["install"]},"application/x-iso9660-image":{"source":"apache","extensions":["iso"]},"application/x-java-archive-diff":{"source":"nginx","extensions":["jardiff"]},"application/x-java-jnlp-file":{"source":"apache","compressible":false,"extensions":["jnlp"]},"application/x-javascript":{"compressible":true},"application/x-keepass2":{"extensions":["kdbx"]},"application/x-latex":{"source":"apache","compressible":false,"extensions":["latex"]},"application/x-lua-bytecode":{"extensions":["luac"]},"application/x-lzh-compressed":{"source":"apache","extensions":["lzh","lha"]},"application/x-makeself":{"source":"nginx","extensions":["run"]},"application/x-mie":{"source":"apache","extensions":["mie"]},"application/x-mobipocket-ebook":{"source":"apache","extensions":["prc","mobi"]},"application/x-mpegurl":{"compressible":false},"application/x-ms-application":{"source":"apache","extensions":["application"]},"application/x-ms-shortcut":{"source":"apache","extensions":["lnk"]},"application/x-ms-wmd":{"source":"apache","extensions":["wmd"]},"application/x-ms-wmz":{"source":"apache","extensions":["wmz"]},"application/x-ms-xbap":{"source":"apache","extensions":["xbap"]},"application/x-msaccess":{"source":"apache","extensions":["mdb"]},"application/x-msbinder":{"source":"apache","extensions":["obd"]},"application/x-mscardfile":{"source":"apache","extensions":["crd"]},"application/x-msclip":{"source":"apache","extensions":["clp"]},"application/x-msdos-program":{"extensions":["exe"]},"application/x-msdownload":{"source":"apache","extensions":["exe","dll","com","bat","msi"]},"application/x-msmediaview":{"source":"apache","extensions":["mvb","m13","m14"]},"application/x-msmetafile":{"source":"apache","extensions":["wmf","wmz","emf","emz"]},"application/x-msmoney":{"source":"apache","extensions":["mny"]},"application/x-mspublisher":{"source":"apache","extensions":["pub"]},"application/x-msschedule":{"source":"apache","extensions":["scd"]},"application/x-msterminal":{"source":"apache","extensions":["trm"]},"application/x-mswrite":{"source":"apache","extensions":["wri"]},"application/x-netcdf":{"source":"apache","extensions":["nc","cdf"]},"application/x-ns-proxy-autoconfig":{"compressible":true,"extensions":["pac"]},"application/x-nzb":{"source":"apache","extensions":["nzb"]},"application/x-perl":{"source":"nginx","extensions":["pl","pm"]},"application/x-pilot":{"source":"nginx","extensions":["prc","pdb"]},"application/x-pkcs12":{"source":"apache","compressible":false,"extensions":["p12","pfx"]},"application/x-pkcs7-certificates":{"source":"apache","extensions":["p7b","spc"]},"application/x-pkcs7-certreqresp":{"source":"apache","extensions":["p7r"]},"application/x-pki-message":{"source":"iana"},"application/x-rar-compressed":{"source":"apache","compressible":false,"extensions":["rar"]},"application/x-redhat-package-manager":{"source":"nginx","extensions":["rpm"]},"application/x-research-info-systems":{"source":"apache","extensions":["ris"]},"application/x-sea":{"source":"nginx","extensions":["sea"]},"application/x-sh":{"source":"apache","compressible":true,"extensions":["sh"]},"application/x-shar":{"source":"apache","extensions":["shar"]},"application/x-shockwave-flash":{"source":"apache","compressible":false,"extensions":["swf"]},"application/x-silverlight-app":{"source":"apache","extensions":["xap"]},"application/x-sql":{"source":"apache","extensions":["sql"]},"application/x-stuffit":{"source":"apache","compressible":false,"extensions":["sit"]},"application/x-stuffitx":{"source":"apache","extensions":["sitx"]},"application/x-subrip":{"source":"apache","extensions":["srt"]},"application/x-sv4cpio":{"source":"apache","extensions":["sv4cpio"]},"application/x-sv4crc":{"source":"apache","extensions":["sv4crc"]},"application/x-t3vm-image":{"source":"apache","extensions":["t3"]},"application/x-tads":{"source":"apache","extensions":["gam"]},"application/x-tar":{"source":"apache","compressible":true,"extensions":["tar"]},"application/x-tcl":{"source":"apache","extensions":["tcl","tk"]},"application/x-tex":{"source":"apache","extensions":["tex"]},"application/x-tex-tfm":{"source":"apache","extensions":["tfm"]},"application/x-texinfo":{"source":"apache","extensions":["texinfo","texi"]},"application/x-tgif":{"source":"apache","extensions":["obj"]},"application/x-ustar":{"source":"apache","extensions":["ustar"]},"application/x-virtualbox-hdd":{"compressible":true,"extensions":["hdd"]},"application/x-virtualbox-ova":{"compressible":true,"extensions":["ova"]},"application/x-virtualbox-ovf":{"compressible":true,"extensions":["ovf"]},"application/x-virtualbox-vbox":{"compressible":true,"extensions":["vbox"]},"application/x-virtualbox-vbox-extpack":{"compressible":false,"extensions":["vbox-extpack"]},"application/x-virtualbox-vdi":{"compressible":true,"extensions":["vdi"]},"application/x-virtualbox-vhd":{"compressible":true,"extensions":["vhd"]},"application/x-virtualbox-vmdk":{"compressible":true,"extensions":["vmdk"]},"application/x-wais-source":{"source":"apache","extensions":["src"]},"application/x-web-app-manifest+json":{"compressible":true,"extensions":["webapp"]},"application/x-www-form-urlencoded":{"source":"iana","compressible":true},"application/x-x509-ca-cert":{"source":"iana","extensions":["der","crt","pem"]},"application/x-x509-ca-ra-cert":{"source":"iana"},"application/x-x509-next-ca-cert":{"source":"iana"},"application/x-xfig":{"source":"apache","extensions":["fig"]},"application/x-xliff+xml":{"source":"apache","compressible":true,"extensions":["xlf"]},"application/x-xpinstall":{"source":"apache","compressible":false,"extensions":["xpi"]},"application/x-xz":{"source":"apache","extensions":["xz"]},"application/x-zmachine":{"source":"apache","extensions":["z1","z2","z3","z4","z5","z6","z7","z8"]},"application/x400-bp":{"source":"iana"},"application/xacml+xml":{"source":"iana","compressible":true},"application/xaml+xml":{"source":"apache","compressible":true,"extensions":["xaml"]},"application/xcap-att+xml":{"source":"iana","compressible":true,"extensions":["xav"]},"application/xcap-caps+xml":{"source":"iana","compressible":true,"extensions":["xca"]},"application/xcap-diff+xml":{"source":"iana","compressible":true,"extensions":["xdf"]},"application/xcap-el+xml":{"source":"iana","compressible":true,"extensions":["xel"]},"application/xcap-error+xml":{"source":"iana","compressible":true,"extensions":["xer"]},"application/xcap-ns+xml":{"source":"iana","compressible":true,"extensions":["xns"]},"application/xcon-conference-info+xml":{"source":"iana","compressible":true},"application/xcon-conference-info-diff+xml":{"source":"iana","compressible":true},"application/xenc+xml":{"source":"iana","compressible":true,"extensions":["xenc"]},"application/xhtml+xml":{"source":"iana","compressible":true,"extensions":["xhtml","xht"]},"application/xhtml-voice+xml":{"source":"apache","compressible":true},"application/xliff+xml":{"source":"iana","compressible":true,"extensions":["xlf"]},"application/xml":{"source":"iana","compressible":true,"extensions":["xml","xsl","xsd","rng"]},"application/xml-dtd":{"source":"iana","compressible":true,"extensions":["dtd"]},"application/xml-external-parsed-entity":{"source":"iana"},"application/xml-patch+xml":{"source":"iana","compressible":true},"application/xmpp+xml":{"source":"iana","compressible":true},"application/xop+xml":{"source":"iana","compressible":true,"extensions":["xop"]},"application/xproc+xml":{"source":"apache","compressible":true,"extensions":["xpl"]},"application/xslt+xml":{"source":"iana","compressible":true,"extensions":["xslt"]},"application/xspf+xml":{"source":"apache","compressible":true,"extensions":["xspf"]},"application/xv+xml":{"source":"iana","compressible":true,"extensions":["mxml","xhvml","xvml","xvm"]},"application/yang":{"source":"iana","extensions":["yang"]},"application/yang-data+json":{"source":"iana","compressible":true},"application/yang-data+xml":{"source":"iana","compressible":true},"application/yang-patch+json":{"source":"iana","compressible":true},"application/yang-patch+xml":{"source":"iana","compressible":true},"application/yin+xml":{"source":"iana","compressible":true,"extensions":["yin"]},"application/zip":{"source":"iana","compressible":false,"extensions":["zip"]},"application/zlib":{"source":"iana"},"application/zstd":{"source":"iana"},"audio/1d-interleaved-parityfec":{"source":"iana"},"audio/32kadpcm":{"source":"iana"},"audio/3gpp":{"source":"iana","compressible":false,"extensions":["3gpp"]},"audio/3gpp2":{"source":"iana"},"audio/aac":{"source":"iana"},"audio/ac3":{"source":"iana"},"audio/adpcm":{"source":"apache","extensions":["adp"]},"audio/amr":{"source":"iana"},"audio/amr-wb":{"source":"iana"},"audio/amr-wb+":{"source":"iana"},"audio/aptx":{"source":"iana"},"audio/asc":{"source":"iana"},"audio/atrac-advanced-lossless":{"source":"iana"},"audio/atrac-x":{"source":"iana"},"audio/atrac3":{"source":"iana"},"audio/basic":{"source":"iana","compressible":false,"extensions":["au","snd"]},"audio/bv16":{"source":"iana"},"audio/bv32":{"source":"iana"},"audio/clearmode":{"source":"iana"},"audio/cn":{"source":"iana"},"audio/dat12":{"source":"iana"},"audio/dls":{"source":"iana"},"audio/dsr-es201108":{"source":"iana"},"audio/dsr-es202050":{"source":"iana"},"audio/dsr-es202211":{"source":"iana"},"audio/dsr-es202212":{"source":"iana"},"audio/dv":{"source":"iana"},"audio/dvi4":{"source":"iana"},"audio/eac3":{"source":"iana"},"audio/encaprtp":{"source":"iana"},"audio/evrc":{"source":"iana"},"audio/evrc-qcp":{"source":"iana"},"audio/evrc0":{"source":"iana"},"audio/evrc1":{"source":"iana"},"audio/evrcb":{"source":"iana"},"audio/evrcb0":{"source":"iana"},"audio/evrcb1":{"source":"iana"},"audio/evrcnw":{"source":"iana"},"audio/evrcnw0":{"source":"iana"},"audio/evrcnw1":{"source":"iana"},"audio/evrcwb":{"source":"iana"},"audio/evrcwb0":{"source":"iana"},"audio/evrcwb1":{"source":"iana"},"audio/evs":{"source":"iana"},"audio/flexfec":{"source":"iana"},"audio/fwdred":{"source":"iana"},"audio/g711-0":{"source":"iana"},"audio/g719":{"source":"iana"},"audio/g722":{"source":"iana"},"audio/g7221":{"source":"iana"},"audio/g723":{"source":"iana"},"audio/g726-16":{"source":"iana"},"audio/g726-24":{"source":"iana"},"audio/g726-32":{"source":"iana"},"audio/g726-40":{"source":"iana"},"audio/g728":{"source":"iana"},"audio/g729":{"source":"iana"},"audio/g7291":{"source":"iana"},"audio/g729d":{"source":"iana"},"audio/g729e":{"source":"iana"},"audio/gsm":{"source":"iana"},"audio/gsm-efr":{"source":"iana"},"audio/gsm-hr-08":{"source":"iana"},"audio/ilbc":{"source":"iana"},"audio/ip-mr_v2.5":{"source":"iana"},"audio/isac":{"source":"apache"},"audio/l16":{"source":"iana"},"audio/l20":{"source":"iana"},"audio/l24":{"source":"iana","compressible":false},"audio/l8":{"source":"iana"},"audio/lpc":{"source":"iana"},"audio/melp":{"source":"iana"},"audio/melp1200":{"source":"iana"},"audio/melp2400":{"source":"iana"},"audio/melp600":{"source":"iana"},"audio/mhas":{"source":"iana"},"audio/midi":{"source":"apache","extensions":["mid","midi","kar","rmi"]},"audio/mobile-xmf":{"source":"iana","extensions":["mxmf"]},"audio/mp3":{"compressible":false,"extensions":["mp3"]},"audio/mp4":{"source":"iana","compressible":false,"extensions":["m4a","mp4a"]},"audio/mp4a-latm":{"source":"iana"},"audio/mpa":{"source":"iana"},"audio/mpa-robust":{"source":"iana"},"audio/mpeg":{"source":"iana","compressible":false,"extensions":["mpga","mp2","mp2a","mp3","m2a","m3a"]},"audio/mpeg4-generic":{"source":"iana"},"audio/musepack":{"source":"apache"},"audio/ogg":{"source":"iana","compressible":false,"extensions":["oga","ogg","spx"]},"audio/opus":{"source":"iana"},"audio/parityfec":{"source":"iana"},"audio/pcma":{"source":"iana"},"audio/pcma-wb":{"source":"iana"},"audio/pcmu":{"source":"iana"},"audio/pcmu-wb":{"source":"iana"},"audio/prs.sid":{"source":"iana"},"audio/qcelp":{"source":"iana"},"audio/raptorfec":{"source":"iana"},"audio/red":{"source":"iana"},"audio/rtp-enc-aescm128":{"source":"iana"},"audio/rtp-midi":{"source":"iana"},"audio/rtploopback":{"source":"iana"},"audio/rtx":{"source":"iana"},"audio/s3m":{"source":"apache","extensions":["s3m"]},"audio/silk":{"source":"apache","extensions":["sil"]},"audio/smv":{"source":"iana"},"audio/smv-qcp":{"source":"iana"},"audio/smv0":{"source":"iana"},"audio/sp-midi":{"source":"iana"},"audio/speex":{"source":"iana"},"audio/t140c":{"source":"iana"},"audio/t38":{"source":"iana"},"audio/telephone-event":{"source":"iana"},"audio/tetra_acelp":{"source":"iana"},"audio/tetra_acelp_bb":{"source":"iana"},"audio/tone":{"source":"iana"},"audio/uemclip":{"source":"iana"},"audio/ulpfec":{"source":"iana"},"audio/usac":{"source":"iana"},"audio/vdvi":{"source":"iana"},"audio/vmr-wb":{"source":"iana"},"audio/vnd.3gpp.iufp":{"source":"iana"},"audio/vnd.4sb":{"source":"iana"},"audio/vnd.audiokoz":{"source":"iana"},"audio/vnd.celp":{"source":"iana"},"audio/vnd.cisco.nse":{"source":"iana"},"audio/vnd.cmles.radio-events":{"source":"iana"},"audio/vnd.cns.anp1":{"source":"iana"},"audio/vnd.cns.inf1":{"source":"iana"},"audio/vnd.dece.audio":{"source":"iana","extensions":["uva","uvva"]},"audio/vnd.digital-winds":{"source":"iana","extensions":["eol"]},"audio/vnd.dlna.adts":{"source":"iana"},"audio/vnd.dolby.heaac.1":{"source":"iana"},"audio/vnd.dolby.heaac.2":{"source":"iana"},"audio/vnd.dolby.mlp":{"source":"iana"},"audio/vnd.dolby.mps":{"source":"iana"},"audio/vnd.dolby.pl2":{"source":"iana"},"audio/vnd.dolby.pl2x":{"source":"iana"},"audio/vnd.dolby.pl2z":{"source":"iana"},"audio/vnd.dolby.pulse.1":{"source":"iana"},"audio/vnd.dra":{"source":"iana","extensions":["dra"]},"audio/vnd.dts":{"source":"iana","extensions":["dts"]},"audio/vnd.dts.hd":{"source":"iana","extensions":["dtshd"]},"audio/vnd.dts.uhd":{"source":"iana"},"audio/vnd.dvb.file":{"source":"iana"},"audio/vnd.everad.plj":{"source":"iana"},"audio/vnd.hns.audio":{"source":"iana"},"audio/vnd.lucent.voice":{"source":"iana","extensions":["lvp"]},"audio/vnd.ms-playready.media.pya":{"source":"iana","extensions":["pya"]},"audio/vnd.nokia.mobile-xmf":{"source":"iana"},"audio/vnd.nortel.vbk":{"source":"iana"},"audio/vnd.nuera.ecelp4800":{"source":"iana","extensions":["ecelp4800"]},"audio/vnd.nuera.ecelp7470":{"source":"iana","extensions":["ecelp7470"]},"audio/vnd.nuera.ecelp9600":{"source":"iana","extensions":["ecelp9600"]},"audio/vnd.octel.sbc":{"source":"iana"},"audio/vnd.presonus.multitrack":{"source":"iana"},"audio/vnd.qcelp":{"source":"iana"},"audio/vnd.rhetorex.32kadpcm":{"source":"iana"},"audio/vnd.rip":{"source":"iana","extensions":["rip"]},"audio/vnd.rn-realaudio":{"compressible":false},"audio/vnd.sealedmedia.softseal.mpeg":{"source":"iana"},"audio/vnd.vmx.cvsd":{"source":"iana"},"audio/vnd.wave":{"compressible":false},"audio/vorbis":{"source":"iana","compressible":false},"audio/vorbis-config":{"source":"iana"},"audio/wav":{"compressible":false,"extensions":["wav"]},"audio/wave":{"compressible":false,"extensions":["wav"]},"audio/webm":{"source":"apache","compressible":false,"extensions":["weba"]},"audio/x-aac":{"source":"apache","compressible":false,"extensions":["aac"]},"audio/x-aiff":{"source":"apache","extensions":["aif","aiff","aifc"]},"audio/x-caf":{"source":"apache","compressible":false,"extensions":["caf"]},"audio/x-flac":{"source":"apache","extensions":["flac"]},"audio/x-m4a":{"source":"nginx","extensions":["m4a"]},"audio/x-matroska":{"source":"apache","extensions":["mka"]},"audio/x-mpegurl":{"source":"apache","extensions":["m3u"]},"audio/x-ms-wax":{"source":"apache","extensions":["wax"]},"audio/x-ms-wma":{"source":"apache","extensions":["wma"]},"audio/x-pn-realaudio":{"source":"apache","extensions":["ram","ra"]},"audio/x-pn-realaudio-plugin":{"source":"apache","extensions":["rmp"]},"audio/x-realaudio":{"source":"nginx","extensions":["ra"]},"audio/x-tta":{"source":"apache"},"audio/x-wav":{"source":"apache","extensions":["wav"]},"audio/xm":{"source":"apache","extensions":["xm"]},"chemical/x-cdx":{"source":"apache","extensions":["cdx"]},"chemical/x-cif":{"source":"apache","extensions":["cif"]},"chemical/x-cmdf":{"source":"apache","extensions":["cmdf"]},"chemical/x-cml":{"source":"apache","extensions":["cml"]},"chemical/x-csml":{"source":"apache","extensions":["csml"]},"chemical/x-pdb":{"source":"apache"},"chemical/x-xyz":{"source":"apache","extensions":["xyz"]},"font/collection":{"source":"iana","extensions":["ttc"]},"font/otf":{"source":"iana","compressible":true,"extensions":["otf"]},"font/sfnt":{"source":"iana"},"font/ttf":{"source":"iana","compressible":true,"extensions":["ttf"]},"font/woff":{"source":"iana","extensions":["woff"]},"font/woff2":{"source":"iana","extensions":["woff2"]},"image/aces":{"source":"iana","extensions":["exr"]},"image/apng":{"compressible":false,"extensions":["apng"]},"image/avci":{"source":"iana"},"image/avcs":{"source":"iana"},"image/bmp":{"source":"iana","compressible":true,"extensions":["bmp"]},"image/cgm":{"source":"iana","extensions":["cgm"]},"image/dicom-rle":{"source":"iana","extensions":["drle"]},"image/emf":{"source":"iana","extensions":["emf"]},"image/fits":{"source":"iana","extensions":["fits"]},"image/g3fax":{"source":"iana","extensions":["g3"]},"image/gif":{"source":"iana","compressible":false,"extensions":["gif"]},"image/heic":{"source":"iana","extensions":["heic"]},"image/heic-sequence":{"source":"iana","extensions":["heics"]},"image/heif":{"source":"iana","extensions":["heif"]},"image/heif-sequence":{"source":"iana","extensions":["heifs"]},"image/hej2k":{"source":"iana","extensions":["hej2"]},"image/hsj2":{"source":"iana","extensions":["hsj2"]},"image/ief":{"source":"iana","extensions":["ief"]},"image/jls":{"source":"iana","extensions":["jls"]},"image/jp2":{"source":"iana","compressible":false,"extensions":["jp2","jpg2"]},"image/jpeg":{"source":"iana","compressible":false,"extensions":["jpeg","jpg","jpe"]},"image/jph":{"source":"iana","extensions":["jph"]},"image/jphc":{"source":"iana","extensions":["jhc"]},"image/jpm":{"source":"iana","compressible":false,"extensions":["jpm"]},"image/jpx":{"source":"iana","compressible":false,"extensions":["jpx","jpf"]},"image/jxr":{"source":"iana","extensions":["jxr"]},"image/jxra":{"source":"iana","extensions":["jxra"]},"image/jxrs":{"source":"iana","extensions":["jxrs"]},"image/jxs":{"source":"iana","extensions":["jxs"]},"image/jxsc":{"source":"iana","extensions":["jxsc"]},"image/jxsi":{"source":"iana","extensions":["jxsi"]},"image/jxss":{"source":"iana","extensions":["jxss"]},"image/ktx":{"source":"iana","extensions":["ktx"]},"image/naplps":{"source":"iana"},"image/pjpeg":{"compressible":false},"image/png":{"source":"iana","compressible":false,"extensions":["png"]},"image/prs.btif":{"source":"iana","extensions":["btif"]},"image/prs.pti":{"source":"iana","extensions":["pti"]},"image/pwg-raster":{"source":"iana"},"image/sgi":{"source":"apache","extensions":["sgi"]},"image/svg+xml":{"source":"iana","compressible":true,"extensions":["svg","svgz"]},"image/t38":{"source":"iana","extensions":["t38"]},"image/tiff":{"source":"iana","compressible":false,"extensions":["tif","tiff"]},"image/tiff-fx":{"source":"iana","extensions":["tfx"]},"image/vnd.adobe.photoshop":{"source":"iana","compressible":true,"extensions":["psd"]},"image/vnd.airzip.accelerator.azv":{"source":"iana","extensions":["azv"]},"image/vnd.cns.inf2":{"source":"iana"},"image/vnd.dece.graphic":{"source":"iana","extensions":["uvi","uvvi","uvg","uvvg"]},"image/vnd.djvu":{"source":"iana","extensions":["djvu","djv"]},"image/vnd.dvb.subtitle":{"source":"iana","extensions":["sub"]},"image/vnd.dwg":{"source":"iana","extensions":["dwg"]},"image/vnd.dxf":{"source":"iana","extensions":["dxf"]},"image/vnd.fastbidsheet":{"source":"iana","extensions":["fbs"]},"image/vnd.fpx":{"source":"iana","extensions":["fpx"]},"image/vnd.fst":{"source":"iana","extensions":["fst"]},"image/vnd.fujixerox.edmics-mmr":{"source":"iana","extensions":["mmr"]},"image/vnd.fujixerox.edmics-rlc":{"source":"iana","extensions":["rlc"]},"image/vnd.globalgraphics.pgb":{"source":"iana"},"image/vnd.microsoft.icon":{"source":"iana","extensions":["ico"]},"image/vnd.mix":{"source":"iana"},"image/vnd.mozilla.apng":{"source":"iana"},"image/vnd.ms-dds":{"extensions":["dds"]},"image/vnd.ms-modi":{"source":"iana","extensions":["mdi"]},"image/vnd.ms-photo":{"source":"apache","extensions":["wdp"]},"image/vnd.net-fpx":{"source":"iana","extensions":["npx"]},"image/vnd.radiance":{"source":"iana"},"image/vnd.sealed.png":{"source":"iana"},"image/vnd.sealedmedia.softseal.gif":{"source":"iana"},"image/vnd.sealedmedia.softseal.jpg":{"source":"iana"},"image/vnd.svf":{"source":"iana"},"image/vnd.tencent.tap":{"source":"iana","extensions":["tap"]},"image/vnd.valve.source.texture":{"source":"iana","extensions":["vtf"]},"image/vnd.wap.wbmp":{"source":"iana","extensions":["wbmp"]},"image/vnd.xiff":{"source":"iana","extensions":["xif"]},"image/vnd.zbrush.pcx":{"source":"iana","extensions":["pcx"]},"image/webp":{"source":"apache","extensions":["webp"]},"image/wmf":{"source":"iana","extensions":["wmf"]},"image/x-3ds":{"source":"apache","extensions":["3ds"]},"image/x-cmu-raster":{"source":"apache","extensions":["ras"]},"image/x-cmx":{"source":"apache","extensions":["cmx"]},"image/x-freehand":{"source":"apache","extensions":["fh","fhc","fh4","fh5","fh7"]},"image/x-icon":{"source":"apache","compressible":true,"extensions":["ico"]},"image/x-jng":{"source":"nginx","extensions":["jng"]},"image/x-mrsid-image":{"source":"apache","extensions":["sid"]},"image/x-ms-bmp":{"source":"nginx","compressible":true,"extensions":["bmp"]},"image/x-pcx":{"source":"apache","extensions":["pcx"]},"image/x-pict":{"source":"apache","extensions":["pic","pct"]},"image/x-portable-anymap":{"source":"apache","extensions":["pnm"]},"image/x-portable-bitmap":{"source":"apache","extensions":["pbm"]},"image/x-portable-graymap":{"source":"apache","extensions":["pgm"]},"image/x-portable-pixmap":{"source":"apache","extensions":["ppm"]},"image/x-rgb":{"source":"apache","extensions":["rgb"]},"image/x-tga":{"source":"apache","extensions":["tga"]},"image/x-xbitmap":{"source":"apache","extensions":["xbm"]},"image/x-xcf":{"compressible":false},"image/x-xpixmap":{"source":"apache","extensions":["xpm"]},"image/x-xwindowdump":{"source":"apache","extensions":["xwd"]},"message/cpim":{"source":"iana"},"message/delivery-status":{"source":"iana"},"message/disposition-notification":{"source":"iana","extensions":["disposition-notification"]},"message/external-body":{"source":"iana"},"message/feedback-report":{"source":"iana"},"message/global":{"source":"iana","extensions":["u8msg"]},"message/global-delivery-status":{"source":"iana","extensions":["u8dsn"]},"message/global-disposition-notification":{"source":"iana","extensions":["u8mdn"]},"message/global-headers":{"source":"iana","extensions":["u8hdr"]},"message/http":{"source":"iana","compressible":false},"message/imdn+xml":{"source":"iana","compressible":true},"message/news":{"source":"iana"},"message/partial":{"source":"iana","compressible":false},"message/rfc822":{"source":"iana","compressible":true,"extensions":["eml","mime"]},"message/s-http":{"source":"iana"},"message/sip":{"source":"iana"},"message/sipfrag":{"source":"iana"},"message/tracking-status":{"source":"iana"},"message/vnd.si.simp":{"source":"iana"},"message/vnd.wfa.wsc":{"source":"iana","extensions":["wsc"]},"model/3mf":{"source":"iana","extensions":["3mf"]},"model/gltf+json":{"source":"iana","compressible":true,"extensions":["gltf"]},"model/gltf-binary":{"source":"iana","compressible":true,"extensions":["glb"]},"model/iges":{"source":"iana","compressible":false,"extensions":["igs","iges"]},"model/mesh":{"source":"iana","compressible":false,"extensions":["msh","mesh","silo"]},"model/mtl":{"source":"iana","extensions":["mtl"]},"model/obj":{"source":"iana","extensions":["obj"]},"model/stl":{"source":"iana","extensions":["stl"]},"model/vnd.collada+xml":{"source":"iana","compressible":true,"extensions":["dae"]},"model/vnd.dwf":{"source":"iana","extensions":["dwf"]},"model/vnd.flatland.3dml":{"source":"iana"},"model/vnd.gdl":{"source":"iana","extensions":["gdl"]},"model/vnd.gs-gdl":{"source":"apache"},"model/vnd.gs.gdl":{"source":"iana"},"model/vnd.gtw":{"source":"iana","extensions":["gtw"]},"model/vnd.moml+xml":{"source":"iana","compressible":true},"model/vnd.mts":{"source":"iana","extensions":["mts"]},"model/vnd.opengex":{"source":"iana","extensions":["ogex"]},"model/vnd.parasolid.transmit.binary":{"source":"iana","extensions":["x_b"]},"model/vnd.parasolid.transmit.text":{"source":"iana","extensions":["x_t"]},"model/vnd.rosette.annotated-data-model":{"source":"iana"},"model/vnd.usdz+zip":{"source":"iana","compressible":false,"extensions":["usdz"]},"model/vnd.valve.source.compiled-map":{"source":"iana","extensions":["bsp"]},"model/vnd.vtu":{"source":"iana","extensions":["vtu"]},"model/vrml":{"source":"iana","compressible":false,"extensions":["wrl","vrml"]},"model/x3d+binary":{"source":"apache","compressible":false,"extensions":["x3db","x3dbz"]},"model/x3d+fastinfoset":{"source":"iana","extensions":["x3db"]},"model/x3d+vrml":{"source":"apache","compressible":false,"extensions":["x3dv","x3dvz"]},"model/x3d+xml":{"source":"iana","compressible":true,"extensions":["x3d","x3dz"]},"model/x3d-vrml":{"source":"iana","extensions":["x3dv"]},"multipart/alternative":{"source":"iana","compressible":false},"multipart/appledouble":{"source":"iana"},"multipart/byteranges":{"source":"iana"},"multipart/digest":{"source":"iana"},"multipart/encrypted":{"source":"iana","compressible":false},"multipart/form-data":{"source":"iana","compressible":false},"multipart/header-set":{"source":"iana"},"multipart/mixed":{"source":"iana"},"multipart/multilingual":{"source":"iana"},"multipart/parallel":{"source":"iana"},"multipart/related":{"source":"iana","compressible":false},"multipart/report":{"source":"iana"},"multipart/signed":{"source":"iana","compressible":false},"multipart/vnd.bint.med-plus":{"source":"iana"},"multipart/voice-message":{"source":"iana"},"multipart/x-mixed-replace":{"source":"iana"},"text/1d-interleaved-parityfec":{"source":"iana"},"text/cache-manifest":{"source":"iana","compressible":true,"extensions":["appcache","manifest"]},"text/calendar":{"source":"iana","extensions":["ics","ifb"]},"text/calender":{"compressible":true},"text/cmd":{"compressible":true},"text/coffeescript":{"extensions":["coffee","litcoffee"]},"text/css":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["css"]},"text/csv":{"source":"iana","compressible":true,"extensions":["csv"]},"text/csv-schema":{"source":"iana"},"text/directory":{"source":"iana"},"text/dns":{"source":"iana"},"text/ecmascript":{"source":"iana"},"text/encaprtp":{"source":"iana"},"text/enriched":{"source":"iana"},"text/flexfec":{"source":"iana"},"text/fwdred":{"source":"iana"},"text/grammar-ref-list":{"source":"iana"},"text/html":{"source":"iana","compressible":true,"extensions":["html","htm","shtml"]},"text/jade":{"extensions":["jade"]},"text/javascript":{"source":"iana","compressible":true},"text/jcr-cnd":{"source":"iana"},"text/jsx":{"compressible":true,"extensions":["jsx"]},"text/less":{"compressible":true,"extensions":["less"]},"text/markdown":{"source":"iana","compressible":true,"extensions":["markdown","md"]},"text/mathml":{"source":"nginx","extensions":["mml"]},"text/mdx":{"compressible":true,"extensions":["mdx"]},"text/mizar":{"source":"iana"},"text/n3":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["n3"]},"text/parameters":{"source":"iana","charset":"UTF-8"},"text/parityfec":{"source":"iana"},"text/plain":{"source":"iana","compressible":true,"extensions":["txt","text","conf","def","list","log","in","ini"]},"text/provenance-notation":{"source":"iana","charset":"UTF-8"},"text/prs.fallenstein.rst":{"source":"iana"},"text/prs.lines.tag":{"source":"iana","extensions":["dsc"]},"text/prs.prop.logic":{"source":"iana"},"text/raptorfec":{"source":"iana"},"text/red":{"source":"iana"},"text/rfc822-headers":{"source":"iana"},"text/richtext":{"source":"iana","compressible":true,"extensions":["rtx"]},"text/rtf":{"source":"iana","compressible":true,"extensions":["rtf"]},"text/rtp-enc-aescm128":{"source":"iana"},"text/rtploopback":{"source":"iana"},"text/rtx":{"source":"iana"},"text/sgml":{"source":"iana","extensions":["sgml","sgm"]},"text/shex":{"extensions":["shex"]},"text/slim":{"extensions":["slim","slm"]},"text/strings":{"source":"iana"},"text/stylus":{"extensions":["stylus","styl"]},"text/t140":{"source":"iana"},"text/tab-separated-values":{"source":"iana","compressible":true,"extensions":["tsv"]},"text/troff":{"source":"iana","extensions":["t","tr","roff","man","me","ms"]},"text/turtle":{"source":"iana","charset":"UTF-8","extensions":["ttl"]},"text/ulpfec":{"source":"iana"},"text/uri-list":{"source":"iana","compressible":true,"extensions":["uri","uris","urls"]},"text/vcard":{"source":"iana","compressible":true,"extensions":["vcard"]},"text/vnd.a":{"source":"iana"},"text/vnd.abc":{"source":"iana"},"text/vnd.ascii-art":{"source":"iana"},"text/vnd.curl":{"source":"iana","extensions":["curl"]},"text/vnd.curl.dcurl":{"source":"apache","extensions":["dcurl"]},"text/vnd.curl.mcurl":{"source":"apache","extensions":["mcurl"]},"text/vnd.curl.scurl":{"source":"apache","extensions":["scurl"]},"text/vnd.debian.copyright":{"source":"iana","charset":"UTF-8"},"text/vnd.dmclientscript":{"source":"iana"},"text/vnd.dvb.subtitle":{"source":"iana","extensions":["sub"]},"text/vnd.esmertec.theme-descriptor":{"source":"iana","charset":"UTF-8"},"text/vnd.ficlab.flt":{"source":"iana"},"text/vnd.fly":{"source":"iana","extensions":["fly"]},"text/vnd.fmi.flexstor":{"source":"iana","extensions":["flx"]},"text/vnd.gml":{"source":"iana"},"text/vnd.graphviz":{"source":"iana","extensions":["gv"]},"text/vnd.hgl":{"source":"iana"},"text/vnd.in3d.3dml":{"source":"iana","extensions":["3dml"]},"text/vnd.in3d.spot":{"source":"iana","extensions":["spot"]},"text/vnd.iptc.newsml":{"source":"iana"},"text/vnd.iptc.nitf":{"source":"iana"},"text/vnd.latex-z":{"source":"iana"},"text/vnd.motorola.reflex":{"source":"iana"},"text/vnd.ms-mediapackage":{"source":"iana"},"text/vnd.net2phone.commcenter.command":{"source":"iana"},"text/vnd.radisys.msml-basic-layout":{"source":"iana"},"text/vnd.senx.warpscript":{"source":"iana"},"text/vnd.si.uricatalogue":{"source":"iana"},"text/vnd.sosi":{"source":"iana"},"text/vnd.sun.j2me.app-descriptor":{"source":"iana","charset":"UTF-8","extensions":["jad"]},"text/vnd.trolltech.linguist":{"source":"iana","charset":"UTF-8"},"text/vnd.wap.si":{"source":"iana"},"text/vnd.wap.sl":{"source":"iana"},"text/vnd.wap.wml":{"source":"iana","extensions":["wml"]},"text/vnd.wap.wmlscript":{"source":"iana","extensions":["wmls"]},"text/vtt":{"source":"iana","charset":"UTF-8","compressible":true,"extensions":["vtt"]},"text/x-asm":{"source":"apache","extensions":["s","asm"]},"text/x-c":{"source":"apache","extensions":["c","cc","cxx","cpp","h","hh","dic"]},"text/x-component":{"source":"nginx","extensions":["htc"]},"text/x-fortran":{"source":"apache","extensions":["f","for","f77","f90"]},"text/x-gwt-rpc":{"compressible":true},"text/x-handlebars-template":{"extensions":["hbs"]},"text/x-java-source":{"source":"apache","extensions":["java"]},"text/x-jquery-tmpl":{"compressible":true},"text/x-lua":{"extensions":["lua"]},"text/x-markdown":{"compressible":true,"extensions":["mkd"]},"text/x-nfo":{"source":"apache","extensions":["nfo"]},"text/x-opml":{"source":"apache","extensions":["opml"]},"text/x-org":{"compressible":true,"extensions":["org"]},"text/x-pascal":{"source":"apache","extensions":["p","pas"]},"text/x-processing":{"compressible":true,"extensions":["pde"]},"text/x-sass":{"extensions":["sass"]},"text/x-scss":{"extensions":["scss"]},"text/x-setext":{"source":"apache","extensions":["etx"]},"text/x-sfv":{"source":"apache","extensions":["sfv"]},"text/x-suse-ymp":{"compressible":true,"extensions":["ymp"]},"text/x-uuencode":{"source":"apache","extensions":["uu"]},"text/x-vcalendar":{"source":"apache","extensions":["vcs"]},"text/x-vcard":{"source":"apache","extensions":["vcf"]},"text/xml":{"source":"iana","compressible":true,"extensions":["xml"]},"text/xml-external-parsed-entity":{"source":"iana"},"text/yaml":{"extensions":["yaml","yml"]},"video/1d-interleaved-parityfec":{"source":"iana"},"video/3gpp":{"source":"iana","extensions":["3gp","3gpp"]},"video/3gpp-tt":{"source":"iana"},"video/3gpp2":{"source":"iana","extensions":["3g2"]},"video/bmpeg":{"source":"iana"},"video/bt656":{"source":"iana"},"video/celb":{"source":"iana"},"video/dv":{"source":"iana"},"video/encaprtp":{"source":"iana"},"video/flexfec":{"source":"iana"},"video/h261":{"source":"iana","extensions":["h261"]},"video/h263":{"source":"iana","extensions":["h263"]},"video/h263-1998":{"source":"iana"},"video/h263-2000":{"source":"iana"},"video/h264":{"source":"iana","extensions":["h264"]},"video/h264-rcdo":{"source":"iana"},"video/h264-svc":{"source":"iana"},"video/h265":{"source":"iana"},"video/iso.segment":{"source":"iana"},"video/jpeg":{"source":"iana","extensions":["jpgv"]},"video/jpeg2000":{"source":"iana"},"video/jpm":{"source":"apache","extensions":["jpm","jpgm"]},"video/mj2":{"source":"iana","extensions":["mj2","mjp2"]},"video/mp1s":{"source":"iana"},"video/mp2p":{"source":"iana"},"video/mp2t":{"source":"iana","extensions":["ts"]},"video/mp4":{"source":"iana","compressible":false,"extensions":["mp4","mp4v","mpg4"]},"video/mp4v-es":{"source":"iana"},"video/mpeg":{"source":"iana","compressible":false,"extensions":["mpeg","mpg","mpe","m1v","m2v"]},"video/mpeg4-generic":{"source":"iana"},"video/mpv":{"source":"iana"},"video/nv":{"source":"iana"},"video/ogg":{"source":"iana","compressible":false,"extensions":["ogv"]},"video/parityfec":{"source":"iana"},"video/pointer":{"source":"iana"},"video/quicktime":{"source":"iana","compressible":false,"extensions":["qt","mov"]},"video/raptorfec":{"source":"iana"},"video/raw":{"source":"iana"},"video/rtp-enc-aescm128":{"source":"iana"},"video/rtploopback":{"source":"iana"},"video/rtx":{"source":"iana"},"video/smpte291":{"source":"iana"},"video/smpte292m":{"source":"iana"},"video/ulpfec":{"source":"iana"},"video/vc1":{"source":"iana"},"video/vc2":{"source":"iana"},"video/vnd.cctv":{"source":"iana"},"video/vnd.dece.hd":{"source":"iana","extensions":["uvh","uvvh"]},"video/vnd.dece.mobile":{"source":"iana","extensions":["uvm","uvvm"]},"video/vnd.dece.mp4":{"source":"iana"},"video/vnd.dece.pd":{"source":"iana","extensions":["uvp","uvvp"]},"video/vnd.dece.sd":{"source":"iana","extensions":["uvs","uvvs"]},"video/vnd.dece.video":{"source":"iana","extensions":["uvv","uvvv"]},"video/vnd.directv.mpeg":{"source":"iana"},"video/vnd.directv.mpeg-tts":{"source":"iana"},"video/vnd.dlna.mpeg-tts":{"source":"iana"},"video/vnd.dvb.file":{"source":"iana","extensions":["dvb"]},"video/vnd.fvt":{"source":"iana","extensions":["fvt"]},"video/vnd.hns.video":{"source":"iana"},"video/vnd.iptvforum.1dparityfec-1010":{"source":"iana"},"video/vnd.iptvforum.1dparityfec-2005":{"source":"iana"},"video/vnd.iptvforum.2dparityfec-1010":{"source":"iana"},"video/vnd.iptvforum.2dparityfec-2005":{"source":"iana"},"video/vnd.iptvforum.ttsavc":{"source":"iana"},"video/vnd.iptvforum.ttsmpeg2":{"source":"iana"},"video/vnd.motorola.video":{"source":"iana"},"video/vnd.motorola.videop":{"source":"iana"},"video/vnd.mpegurl":{"source":"iana","extensions":["mxu","m4u"]},"video/vnd.ms-playready.media.pyv":{"source":"iana","extensions":["pyv"]},"video/vnd.nokia.interleaved-multimedia":{"source":"iana"},"video/vnd.nokia.mp4vr":{"source":"iana"},"video/vnd.nokia.videovoip":{"source":"iana"},"video/vnd.objectvideo":{"source":"iana"},"video/vnd.radgamettools.bink":{"source":"iana"},"video/vnd.radgamettools.smacker":{"source":"iana"},"video/vnd.sealed.mpeg1":{"source":"iana"},"video/vnd.sealed.mpeg4":{"source":"iana"},"video/vnd.sealed.swf":{"source":"iana"},"video/vnd.sealedmedia.softseal.mov":{"source":"iana"},"video/vnd.uvvu.mp4":{"source":"iana","extensions":["uvu","uvvu"]},"video/vnd.vivo":{"source":"iana","extensions":["viv"]},"video/vnd.youtube.yt":{"source":"iana"},"video/vp8":{"source":"iana"},"video/webm":{"source":"apache","compressible":false,"extensions":["webm"]},"video/x-f4v":{"source":"apache","extensions":["f4v"]},"video/x-fli":{"source":"apache","extensions":["fli"]},"video/x-flv":{"source":"apache","compressible":false,"extensions":["flv"]},"video/x-m4v":{"source":"apache","extensions":["m4v"]},"video/x-matroska":{"source":"apache","compressible":false,"extensions":["mkv","mk3d","mks"]},"video/x-mng":{"source":"apache","extensions":["mng"]},"video/x-ms-asf":{"source":"apache","extensions":["asf","asx"]},"video/x-ms-vob":{"source":"apache","extensions":["vob"]},"video/x-ms-wm":{"source":"apache","extensions":["wm"]},"video/x-ms-wmv":{"source":"apache","compressible":false,"extensions":["wmv"]},"video/x-ms-wmx":{"source":"apache","extensions":["wmx"]},"video/x-ms-wvx":{"source":"apache","extensions":["wvx"]},"video/x-msvideo":{"source":"apache","extensions":["avi"]},"video/x-sgi-movie":{"source":"apache","extensions":["movie"]},"video/x-smv":{"source":"apache","extensions":["smv"]},"x-conference/x-cooltalk":{"source":"apache","extensions":["ice"]},"x-shader/x-fragment":{"compressible":true},"x-shader/x-vertex":{"compressible":true}}; + +/***/ }), + +/***/ 125: +/***/ (function(module) { + +// API +module.exports = state; + +/** + * Creates initial state object + * for iteration over list + * + * @param {array|object} list - list to iterate over + * @param {function|null} sortMethod - function to use for keys sort, + * or `null` to keep them as is + * @returns {object} - initial state object + */ +function state(list, sortMethod) +{ + var isNamedList = !Array.isArray(list) + , initState = + { + index : 0, + keyedList: isNamedList || sortMethod ? Object.keys(list) : null, + jobs : {}, + results : isNamedList ? {} : [], + size : isNamedList ? Object.keys(list).length : list.length + } + ; + + if (sortMethod) + { + // sort array keys based on it's values + // sort object's keys just on own merit + initState.keyedList.sort(isNamedList ? sortMethod : function(a, b) + { + return sortMethod(list[a], list[b]); + }); + } + + return initState; +} + + +/***/ }), + +/***/ 128: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/*! + * mime-db + * Copyright(c) 2014 Jonathan Ong + * MIT Licensed + */ + +/** + * Module exports. + */ + +module.exports = __webpack_require__(118) + + +/***/ }), + +/***/ 154: +/***/ (function(module) { + +module.exports = defer; + +/** + * Runs provided function on next iteration of the event loop + * + * @param {function} fn - function to run + */ +function defer(fn) +{ + var nextTick = typeof setImmediate == 'function' + ? setImmediate + : ( + typeof process == 'object' && typeof process.nextTick == 'function' + ? process.nextTick + : null + ); + + if (nextTick) + { + nextTick(fn); + } + else + { + setTimeout(fn, 0); + } +} + + +/***/ }), + +/***/ 164: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var Stream = __webpack_require__(413).Stream; +var util = __webpack_require__(669); + +module.exports = DelayedStream; +function DelayedStream() { + this.source = null; + this.dataSize = 0; + this.maxDataSize = 1024 * 1024; + this.pauseStream = true; + + this._maxDataSizeExceeded = false; + this._released = false; + this._bufferedEvents = []; +} +util.inherits(DelayedStream, Stream); + +DelayedStream.create = function(source, options) { + var delayedStream = new this(); + + options = options || {}; + for (var option in options) { + delayedStream[option] = options[option]; + } + + delayedStream.source = source; + + var realEmit = source.emit; + source.emit = function() { + delayedStream._handleEmit(arguments); + return realEmit.apply(source, arguments); + }; + + source.on('error', function() {}); + if (delayedStream.pauseStream) { + source.pause(); + } + + return delayedStream; +}; + +Object.defineProperty(DelayedStream.prototype, 'readable', { + configurable: true, + enumerable: true, + get: function() { + return this.source.readable; + } +}); + +DelayedStream.prototype.setEncoding = function() { + return this.source.setEncoding.apply(this.source, arguments); +}; + +DelayedStream.prototype.resume = function() { + if (!this._released) { + this.release(); + } + + this.source.resume(); +}; + +DelayedStream.prototype.pause = function() { + this.source.pause(); +}; + +DelayedStream.prototype.release = function() { + this._released = true; + + this._bufferedEvents.forEach(function(args) { + this.emit.apply(this, args); + }.bind(this)); + this._bufferedEvents = []; +}; + +DelayedStream.prototype.pipe = function() { + var r = Stream.prototype.pipe.apply(this, arguments); + this.resume(); + return r; +}; + +DelayedStream.prototype._handleEmit = function(args) { + if (this._released) { + this.emit.apply(this, args); + return; + } + + if (args[0] === 'data') { + this.dataSize += args[1].length; + this._checkIfMaxDataSizeExceeded(); + } + + this._bufferedEvents.push(args); +}; + +DelayedStream.prototype._checkIfMaxDataSizeExceeded = function() { + if (this._maxDataSizeExceeded) { + return; + } + + if (this.dataSize <= this.maxDataSize) { + return; + } + + this._maxDataSizeExceeded = true; + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.' + this.emit('error', new Error(message)); +}; + + +/***/ }), + +/***/ 176: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var serialOrdered = __webpack_require__(275); + +// Public API +module.exports = serial; + +/** + * Runs iterator over provided array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serial(list, iterator, callback) +{ + return serialOrdered(list, iterator, null, callback); +} + + +/***/ }), + +/***/ 211: +/***/ (function(module) { + +module.exports = require("https"); + +/***/ }), + +/***/ 213: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var util = __webpack_require__(669); +var Stream = __webpack_require__(413).Stream; +var DelayedStream = __webpack_require__(164); + +module.exports = CombinedStream; +function CombinedStream() { + this.writable = false; + this.readable = true; + this.dataSize = 0; + this.maxDataSize = 2 * 1024 * 1024; + this.pauseStreams = true; + + this._released = false; + this._streams = []; + this._currentStream = null; + this._insideLoop = false; + this._pendingNext = false; +} +util.inherits(CombinedStream, Stream); + +CombinedStream.create = function(options) { + var combinedStream = new this(); + + options = options || {}; + for (var option in options) { + combinedStream[option] = options[option]; + } + + return combinedStream; +}; + +CombinedStream.isStreamLike = function(stream) { + return (typeof stream !== 'function') + && (typeof stream !== 'string') + && (typeof stream !== 'boolean') + && (typeof stream !== 'number') + && (!Buffer.isBuffer(stream)); +}; + +CombinedStream.prototype.append = function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + + if (isStreamLike) { + if (!(stream instanceof DelayedStream)) { + var newStream = DelayedStream.create(stream, { + maxDataSize: Infinity, + pauseStream: this.pauseStreams, + }); + stream.on('data', this._checkDataSize.bind(this)); + stream = newStream; + } + + this._handleErrors(stream); + + if (this.pauseStreams) { + stream.pause(); + } + } + + this._streams.push(stream); + return this; +}; + +CombinedStream.prototype.pipe = function(dest, options) { + Stream.prototype.pipe.call(this, dest, options); + this.resume(); + return dest; +}; + +CombinedStream.prototype._getNext = function() { + this._currentStream = null; + + if (this._insideLoop) { + this._pendingNext = true; + return; // defer call + } + + this._insideLoop = true; + try { + do { + this._pendingNext = false; + this._realGetNext(); + } while (this._pendingNext); + } finally { + this._insideLoop = false; + } +}; + +CombinedStream.prototype._realGetNext = function() { + var stream = this._streams.shift(); + + + if (typeof stream == 'undefined') { + this.end(); + return; + } + + if (typeof stream !== 'function') { + this._pipeNext(stream); + return; + } + + var getStream = stream; + getStream(function(stream) { + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('data', this._checkDataSize.bind(this)); + this._handleErrors(stream); + } + + this._pipeNext(stream); + }.bind(this)); +}; + +CombinedStream.prototype._pipeNext = function(stream) { + this._currentStream = stream; + + var isStreamLike = CombinedStream.isStreamLike(stream); + if (isStreamLike) { + stream.on('end', this._getNext.bind(this)); + stream.pipe(this, {end: false}); + return; + } + + var value = stream; + this.write(value); + this._getNext(); +}; + +CombinedStream.prototype._handleErrors = function(stream) { + var self = this; + stream.on('error', function(err) { + self._emitError(err); + }); +}; + +CombinedStream.prototype.write = function(data) { + this.emit('data', data); +}; + +CombinedStream.prototype.pause = function() { + if (!this.pauseStreams) { + return; + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.pause) == 'function') this._currentStream.pause(); + this.emit('pause'); +}; + +CombinedStream.prototype.resume = function() { + if (!this._released) { + this._released = true; + this.writable = true; + this._getNext(); + } + + if(this.pauseStreams && this._currentStream && typeof(this._currentStream.resume) == 'function') this._currentStream.resume(); + this.emit('resume'); +}; + +CombinedStream.prototype.end = function() { + this._reset(); + this.emit('end'); +}; + +CombinedStream.prototype.destroy = function() { + this._reset(); + this.emit('close'); +}; + +CombinedStream.prototype._reset = function() { + this.writable = false; + this._streams = []; + this._currentStream = null; +}; + +CombinedStream.prototype._checkDataSize = function() { + this._updateDataSize(); + if (this.dataSize <= this.maxDataSize) { + return; + } + + var message = + 'DelayedStream#maxDataSize of ' + this.maxDataSize + ' bytes exceeded.'; + this._emitError(new Error(message)); +}; + +CombinedStream.prototype._updateDataSize = function() { + this.dataSize = 0; + + var self = this; + this._streams.forEach(function(stream) { + if (!stream.dataSize) { + return; + } + + self.dataSize += stream.dataSize; + }); + + if (this._currentStream && this._currentStream.dataSize) { + this.dataSize += this._currentStream.dataSize; + } +}; + +CombinedStream.prototype._emitError = function(err) { + this._reset(); + this.emit('error', err); +}; + + +/***/ }), + +/***/ 258: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var async = __webpack_require__(792) + , abort = __webpack_require__(762) + ; + +// API +module.exports = iterate; + +/** + * Iterates over each job object + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {object} state - current job status + * @param {function} callback - invoked when all elements processed + */ +function iterate(list, iterator, state, callback) +{ + // store current index + var key = state['keyedList'] ? state['keyedList'][state.index] : state.index; + + state.jobs[key] = runJob(iterator, key, list[key], function(error, output) + { + // don't repeat yourself + // skip secondary callbacks + if (!(key in state.jobs)) + { + return; + } + + // clean up jobs + delete state.jobs[key]; + + if (error) + { + // don't process rest of the results + // stop still active jobs + // and reset the list + abort(state); + } + else + { + state.results[key] = output; + } + + // return salvaged results + callback(error, state.results); + }); +} + +/** + * Runs iterator over provided job element + * + * @param {function} iterator - iterator to invoke + * @param {string|number} key - key/index of the element in the list of jobs + * @param {mixed} item - job description + * @param {function} callback - invoked after iterator is done with the job + * @returns {function|mixed} - job abort function or something else + */ +function runJob(iterator, key, item, callback) +{ + var aborter; + + // allow shortcut if iterator expects only two arguments + if (iterator.length == 2) + { + aborter = iterator(item, async(callback)); + } + // otherwise go with full three arguments + else + { + aborter = iterator(item, key, async(callback)); + } + + return aborter; +} + + +/***/ }), + +/***/ 275: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var iterate = __webpack_require__(258) + , initState = __webpack_require__(125) + , terminator = __webpack_require__(2) + ; + +// Public API +module.exports = serialOrdered; +// sorting helpers +module.exports.ascending = ascending; +module.exports.descending = descending; + +/** + * Runs iterator over provided sorted array elements in series + * + * @param {array|object} list - array or object (named list) to iterate over + * @param {function} iterator - iterator to run + * @param {function} sortMethod - custom sort function + * @param {function} callback - invoked when all elements processed + * @returns {function} - jobs terminator + */ +function serialOrdered(list, iterator, sortMethod, callback) +{ + var state = initState(list, sortMethod); + + iterate(list, iterator, state, function iteratorHandler(error, result) + { + if (error) + { + callback(error, result); + return; + } + + state.index++; + + // are we there yet? + if (state.index < (state['keyedList'] || list).length) + { + iterate(list, iterator, state, iteratorHandler); + return; + } + + // done here + callback(null, state.results); + }); + + return terminator.bind(state, callback); +} + +/* + * -- Sort methods + */ + +/** + * sort helper to sort array elements in ascending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function ascending(a, b) +{ + return a < b ? -1 : a > b ? 1 : 0; +} + +/** + * sort helper to sort array elements in descending order + * + * @param {mixed} a - an item to compare + * @param {mixed} b - an item to compare + * @returns {number} - comparison result + */ +function descending(a, b) +{ + return -1 * ascending(a, b); +} + + +/***/ }), + +/***/ 294: +/***/ (function(module, __unusedexports, __webpack_require__) { + +module.exports = +{ + parallel : __webpack_require__(89), + serial : __webpack_require__(176), + serialOrdered : __webpack_require__(275) +}; + + +/***/ }), + +/***/ 413: +/***/ (function(module) { + +module.exports = require("stream"); + +/***/ }), + +/***/ 416: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var CombinedStream = __webpack_require__(213); +var util = __webpack_require__(669); +var path = __webpack_require__(622); +var http = __webpack_require__(605); +var https = __webpack_require__(211); +var parseUrl = __webpack_require__(835).parse; +var fs = __webpack_require__(747); +var mime = __webpack_require__(769); +var asynckit = __webpack_require__(294); +var populate = __webpack_require__(70); + +// Public API +module.exports = FormData; + +// make it a Stream +util.inherits(FormData, CombinedStream); + +/** + * Create readable "multipart/form-data" streams. + * Can be used to submit forms + * and file uploads to other web applications. + * + * @constructor + * @param {Object} options - Properties to be added/overriden for FormData and CombinedStream + */ +function FormData(options) { + if (!(this instanceof FormData)) { + return new FormData(); + } + + this._overheadLength = 0; + this._valueLength = 0; + this._valuesToMeasure = []; + + CombinedStream.call(this); + + options = options || {}; + for (var option in options) { + this[option] = options[option]; + } +} + +FormData.LINE_BREAK = '\r\n'; +FormData.DEFAULT_CONTENT_TYPE = 'application/octet-stream'; + +FormData.prototype.append = function(field, value, options) { + + options = options || {}; + + // allow filename as single option + if (typeof options == 'string') { + options = {filename: options}; + } + + var append = CombinedStream.prototype.append.bind(this); + + // all that streamy business can't handle numbers + if (typeof value == 'number') { + value = '' + value; + } + + // https://github.com/felixge/node-form-data/issues/38 + if (util.isArray(value)) { + // Please convert your array into string + // the way web server expects it + this._error(new Error('Arrays are not supported.')); + return; + } + + var header = this._multiPartHeader(field, value, options); + var footer = this._multiPartFooter(); + + append(header); + append(value); + append(footer); + + // pass along options.knownLength + this._trackLength(header, value, options); +}; + +FormData.prototype._trackLength = function(header, value, options) { + var valueLength = 0; + + // used w/ getLengthSync(), when length is known. + // e.g. for streaming directly from a remote server, + // w/ a known file a size, and not wanting to wait for + // incoming file to finish to get its size. + if (options.knownLength != null) { + valueLength += +options.knownLength; + } else if (Buffer.isBuffer(value)) { + valueLength = value.length; + } else if (typeof value === 'string') { + valueLength = Buffer.byteLength(value); + } + + this._valueLength += valueLength; + + // @check why add CRLF? does this account for custom/multiple CRLFs? + this._overheadLength += + Buffer.byteLength(header) + + FormData.LINE_BREAK.length; + + // empty or either doesn't have path or not an http response + if (!value || ( !value.path && !(value.readable && value.hasOwnProperty('httpVersion')) )) { + return; + } + + // no need to bother with the length + if (!options.knownLength) { + this._valuesToMeasure.push(value); + } +}; + +FormData.prototype._lengthRetriever = function(value, callback) { + + if (value.hasOwnProperty('fd')) { + + // take read range into a account + // `end` = Infinity –> read file till the end + // + // TODO: Looks like there is bug in Node fs.createReadStream + // it doesn't respect `end` options without `start` options + // Fix it when node fixes it. + // https://github.com/joyent/node/issues/7819 + if (value.end != undefined && value.end != Infinity && value.start != undefined) { + + // when end specified + // no need to calculate range + // inclusive, starts with 0 + callback(null, value.end + 1 - (value.start ? value.start : 0)); + + // not that fast snoopy + } else { + // still need to fetch file size from fs + fs.stat(value.path, function(err, stat) { + + var fileSize; + + if (err) { + callback(err); + return; + } + + // update final size based on the range options + fileSize = stat.size - (value.start ? value.start : 0); + callback(null, fileSize); + }); + } + + // or http response + } else if (value.hasOwnProperty('httpVersion')) { + callback(null, +value.headers['content-length']); + + // or request stream http://github.com/mikeal/request + } else if (value.hasOwnProperty('httpModule')) { + // wait till response come back + value.on('response', function(response) { + value.pause(); + callback(null, +response.headers['content-length']); + }); + value.resume(); + + // something else + } else { + callback('Unknown stream'); + } +}; + +FormData.prototype._multiPartHeader = function(field, value, options) { + // custom header specified (as string)? + // it becomes responsible for boundary + // (e.g. to handle extra CRLFs on .NET servers) + if (typeof options.header == 'string') { + return options.header; + } + + var contentDisposition = this._getContentDisposition(value, options); + var contentType = this._getContentType(value, options); + + var contents = ''; + var headers = { + // add custom disposition as third element or keep it two elements if not + 'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []), + // if no content type. allow it to be empty array + 'Content-Type': [].concat(contentType || []) + }; + + // allow custom headers. + if (typeof options.header == 'object') { + populate(headers, options.header); + } + + var header; + for (var prop in headers) { + if (!headers.hasOwnProperty(prop)) continue; + header = headers[prop]; + + // skip nullish headers. + if (header == null) { + continue; + } + + // convert all headers to arrays. + if (!Array.isArray(header)) { + header = [header]; + } + + // add non-empty headers. + if (header.length) { + contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK; + } + } + + return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK; +}; + +FormData.prototype._getContentDisposition = function(value, options) { + + var filename + , contentDisposition + ; + + if (typeof options.filepath === 'string') { + // custom filepath for relative paths + filename = path.normalize(options.filepath).replace(/\\/g, '/'); + } else if (options.filename || value.name || value.path) { + // custom filename take precedence + // formidable and the browser add a name property + // fs- and request- streams have path property + filename = path.basename(options.filename || value.name || value.path); + } else if (value.readable && value.hasOwnProperty('httpVersion')) { + // or try http response + filename = path.basename(value.client._httpMessage.path || ''); + } + + if (filename) { + contentDisposition = 'filename="' + filename + '"'; + } + + return contentDisposition; +}; + +FormData.prototype._getContentType = function(value, options) { + + // use custom content-type above all + var contentType = options.contentType; + + // or try `name` from formidable, browser + if (!contentType && value.name) { + contentType = mime.lookup(value.name); + } + + // or try `path` from fs-, request- streams + if (!contentType && value.path) { + contentType = mime.lookup(value.path); + } + + // or if it's http-reponse + if (!contentType && value.readable && value.hasOwnProperty('httpVersion')) { + contentType = value.headers['content-type']; + } + + // or guess it from the filepath or filename + if (!contentType && (options.filepath || options.filename)) { + contentType = mime.lookup(options.filepath || options.filename); + } + + // fallback to the default content type if `value` is not simple value + if (!contentType && typeof value == 'object') { + contentType = FormData.DEFAULT_CONTENT_TYPE; + } + + return contentType; +}; + +FormData.prototype._multiPartFooter = function() { + return function(next) { + var footer = FormData.LINE_BREAK; + + var lastPart = (this._streams.length === 0); + if (lastPart) { + footer += this._lastBoundary(); + } + + next(footer); + }.bind(this); +}; + +FormData.prototype._lastBoundary = function() { + return '--' + this.getBoundary() + '--' + FormData.LINE_BREAK; +}; + +FormData.prototype.getHeaders = function(userHeaders) { + var header; + var formHeaders = { + 'content-type': 'multipart/form-data; boundary=' + this.getBoundary() + }; + + for (header in userHeaders) { + if (userHeaders.hasOwnProperty(header)) { + formHeaders[header.toLowerCase()] = userHeaders[header]; + } + } + + return formHeaders; +}; + +FormData.prototype.getBoundary = function() { + if (!this._boundary) { + this._generateBoundary(); + } + + return this._boundary; +}; + +FormData.prototype.getBuffer = function() { + var dataBuffer = new Buffer.alloc( 0 ); + var boundary = this.getBoundary(); + + // Create the form content. Add Line breaks to the end of data. + for (var i = 0, len = this._streams.length; i < len; i++) { + if (typeof this._streams[i] !== 'function') { + + // Add content to the buffer. + if(Buffer.isBuffer(this._streams[i])) { + dataBuffer = Buffer.concat( [dataBuffer, this._streams[i]]); + }else { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(this._streams[i])]); + } + + // Add break after content. + if (typeof this._streams[i] !== 'string' || this._streams[i].substring( 2, boundary.length + 2 ) !== boundary) { + dataBuffer = Buffer.concat( [dataBuffer, Buffer.from(FormData.LINE_BREAK)] ); + } + } + } + + // Add the footer and return the Buffer object. + return Buffer.concat( [dataBuffer, Buffer.from(this._lastBoundary())] ); +}; + +FormData.prototype._generateBoundary = function() { + // This generates a 50 character boundary similar to those used by Firefox. + // They are optimized for boyer-moore parsing. + var boundary = '--------------------------'; + for (var i = 0; i < 24; i++) { + boundary += Math.floor(Math.random() * 10).toString(16); + } + + this._boundary = boundary; +}; + +// Note: getLengthSync DOESN'T calculate streams length +// As workaround one can calculate file size manually +// and add it as knownLength option +FormData.prototype.getLengthSync = function() { + var knownLength = this._overheadLength + this._valueLength; + + // Don't get confused, there are 3 "internal" streams for each keyval pair + // so it basically checks if there is any value added to the form + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + // https://github.com/form-data/form-data/issues/40 + if (!this.hasKnownLength()) { + // Some async length retrievers are present + // therefore synchronous length calculation is false. + // Please use getLength(callback) to get proper length + this._error(new Error('Cannot calculate proper length in synchronous way.')); + } + + return knownLength; +}; + +// Public API to check if length of added values is known +// https://github.com/form-data/form-data/issues/196 +// https://github.com/form-data/form-data/issues/262 +FormData.prototype.hasKnownLength = function() { + var hasKnownLength = true; + + if (this._valuesToMeasure.length) { + hasKnownLength = false; + } + + return hasKnownLength; +}; + +FormData.prototype.getLength = function(cb) { + var knownLength = this._overheadLength + this._valueLength; + + if (this._streams.length) { + knownLength += this._lastBoundary().length; + } + + if (!this._valuesToMeasure.length) { + process.nextTick(cb.bind(this, null, knownLength)); + return; + } + + asynckit.parallel(this._valuesToMeasure, this._lengthRetriever, function(err, values) { + if (err) { + cb(err); + return; + } + + values.forEach(function(length) { + knownLength += length; + }); + + cb(null, knownLength); + }); +}; + +FormData.prototype.submit = function(params, cb) { + var request + , options + , defaults = {method: 'post'} + ; + + // parse provided url if it's string + // or treat it as options object + if (typeof params == 'string') { + + params = parseUrl(params); + options = populate({ + port: params.port, + path: params.pathname, + host: params.hostname, + protocol: params.protocol + }, defaults); + + // use custom params + } else { + + options = populate(params, defaults); + // if no port provided use default one + if (!options.port) { + options.port = options.protocol == 'https:' ? 443 : 80; + } + } + + // put that good code in getHeaders to some use + options.headers = this.getHeaders(params.headers); + + // https if specified, fallback to http in any other case + if (options.protocol == 'https:') { + request = https.request(options); + } else { + request = http.request(options); + } + + // get content length and fire away + this.getLength(function(err, length) { + if (err) { + this._error(err); + return; + } + + // add content length + request.setHeader('Content-Length', length); + + this.pipe(request); + if (cb) { + request.on('error', cb); + request.on('response', cb.bind(this, null)); + } + }.bind(this)); + + return request; +}; + +FormData.prototype._error = function(err) { + if (!this.error) { + this.error = err; + this.pause(); + this.emit('error', err); + } +}; + +FormData.prototype.toString = function () { + return '[object FormData]'; +}; + + +/***/ }), + +/***/ 417: +/***/ (function(module) { + +module.exports = require("crypto"); + +/***/ }), + +/***/ 431: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const os = __importStar(__webpack_require__(87)); +const utils_1 = __webpack_require__(82); +/** + * Commands + * + * Command Format: + * ::name key=value,key=value::message + * + * Examples: + * ::warning::This is the message + * ::set-env name=MY_VAR::some value + */ +function issueCommand(command, properties, message) { + const cmd = new Command(command, properties, message); + process.stdout.write(cmd.toString() + os.EOL); +} +exports.issueCommand = issueCommand; +function issue(name, message = '') { + issueCommand(name, {}, message); +} +exports.issue = issue; +const CMD_STRING = '::'; +class Command { + constructor(command, properties, message) { + if (!command) { + command = 'missing.command'; + } + this.command = command; + this.properties = properties; + this.message = message; + } + toString() { + let cmdStr = CMD_STRING + this.command; + if (this.properties && Object.keys(this.properties).length > 0) { + cmdStr += ' '; + let first = true; + for (const key in this.properties) { + if (this.properties.hasOwnProperty(key)) { + const val = this.properties[key]; + if (val) { + if (first) { + first = false; + } + else { + cmdStr += ','; + } + cmdStr += `${key}=${escapeProperty(val)}`; + } + } + } + } + cmdStr += `${CMD_STRING}${escapeData(this.message)}`; + return cmdStr; + } +} +function escapeData(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A'); +} +function escapeProperty(s) { + return utils_1.toCommandValue(s) + .replace(/%/g, '%25') + .replace(/\r/g, '%0D') + .replace(/\n/g, '%0A') + .replace(/:/g, '%3A') + .replace(/,/g, '%2C'); +} +//# sourceMappingURL=command.js.map + +/***/ }), + +/***/ 470: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const command_1 = __webpack_require__(431); +const file_command_1 = __webpack_require__(102); +const utils_1 = __webpack_require__(82); +const os = __importStar(__webpack_require__(87)); +const path = __importStar(__webpack_require__(622)); +/** + * The code to exit an action + */ +var ExitCode; +(function (ExitCode) { + /** + * A code indicating that the action was successful + */ + ExitCode[ExitCode["Success"] = 0] = "Success"; + /** + * A code indicating that the action was a failure + */ + ExitCode[ExitCode["Failure"] = 1] = "Failure"; +})(ExitCode = exports.ExitCode || (exports.ExitCode = {})); +//----------------------------------------------------------------------- +// Variables +//----------------------------------------------------------------------- +/** + * Sets env variable for this action and future actions in the job + * @param name the name of the variable to set + * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function exportVariable(name, val) { + const convertedVal = utils_1.toCommandValue(val); + process.env[name] = convertedVal; + const filePath = process.env['GITHUB_ENV'] || ''; + if (filePath) { + const delimiter = '_GitHubActionsFileCommandDelimeter_'; + const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; + file_command_1.issueCommand('ENV', commandValue); + } + else { + command_1.issueCommand('set-env', { name }, convertedVal); + } +} +exports.exportVariable = exportVariable; +/** + * Registers a secret which will get masked from logs + * @param secret value of the secret + */ +function setSecret(secret) { + command_1.issueCommand('add-mask', {}, secret); +} +exports.setSecret = setSecret; +/** + * Prepends inputPath to the PATH (for this action and future actions) + * @param inputPath + */ +function addPath(inputPath) { + const filePath = process.env['GITHUB_PATH'] || ''; + if (filePath) { + file_command_1.issueCommand('PATH', inputPath); + } + else { + command_1.issueCommand('add-path', {}, inputPath); + } + process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; +} +exports.addPath = addPath; +/** + * Gets the value of an input. The value is also trimmed. + * + * @param name name of the input to get + * @param options optional. See InputOptions. + * @returns string + */ +function getInput(name, options) { + const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; + if (options && options.required && !val) { + throw new Error(`Input required and not supplied: ${name}`); + } + return val.trim(); +} +exports.getInput = getInput; +/** + * Sets the value of an output. + * + * @param name name of the output to set + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function setOutput(name, value) { + command_1.issueCommand('set-output', { name }, value); +} +exports.setOutput = setOutput; +/** + * Enables or disables the echoing of commands into stdout for the rest of the step. + * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. + * + */ +function setCommandEcho(enabled) { + command_1.issue('echo', enabled ? 'on' : 'off'); +} +exports.setCommandEcho = setCommandEcho; +//----------------------------------------------------------------------- +// Results +//----------------------------------------------------------------------- +/** + * Sets the action status to failed. + * When the action exits it will be with an exit code of 1 + * @param message add error issue message + */ +function setFailed(message) { + process.exitCode = ExitCode.Failure; + error(message); +} +exports.setFailed = setFailed; +//----------------------------------------------------------------------- +// Logging Commands +//----------------------------------------------------------------------- +/** + * Gets whether Actions Step Debug is on or not + */ +function isDebug() { + return process.env['RUNNER_DEBUG'] === '1'; +} +exports.isDebug = isDebug; +/** + * Writes debug message to user log + * @param message debug message + */ +function debug(message) { + command_1.issueCommand('debug', {}, message); +} +exports.debug = debug; +/** + * Adds an error issue + * @param message error issue message. Errors will be converted to string via toString() + */ +function error(message) { + command_1.issue('error', message instanceof Error ? message.toString() : message); +} +exports.error = error; +/** + * Adds an warning issue + * @param message warning issue message. Errors will be converted to string via toString() + */ +function warning(message) { + command_1.issue('warning', message instanceof Error ? message.toString() : message); +} +exports.warning = warning; +/** + * Writes info to log with console.log. + * @param message info message + */ +function info(message) { + process.stdout.write(message + os.EOL); +} +exports.info = info; +/** + * Begin an output group. + * + * Output until the next `groupEnd` will be foldable in this group + * + * @param name The name of the output group + */ +function startGroup(name) { + command_1.issue('group', name); +} +exports.startGroup = startGroup; +/** + * End an output group. + */ +function endGroup() { + command_1.issue('endgroup'); +} +exports.endGroup = endGroup; +/** + * Wrap an asynchronous function call in a group. + * + * Returns the same type as the function itself. + * + * @param name The name of the group + * @param fn The function to wrap in the group + */ +function group(name, fn) { + return __awaiter(this, void 0, void 0, function* () { + startGroup(name); + let result; + try { + result = yield fn(); + } + finally { + endGroup(); + } + return result; + }); +} +exports.group = group; +//----------------------------------------------------------------------- +// Wrapper action state +//----------------------------------------------------------------------- +/** + * Saves state for current action, the state can only be retrieved by this action's post job execution. + * + * @param name name of the state to store + * @param value value to store. Non-string values will be converted to a string via JSON.stringify + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function saveState(name, value) { + command_1.issueCommand('save-state', { name }, value); +} +exports.saveState = saveState; +/** + * Gets the value of an state set by this action's main execution. + * + * @param name name of the state to get + * @returns string + */ +function getState(name) { + return process.env[`STATE_${name}`] || ''; +} +exports.getState = getState; +//# sourceMappingURL=core.js.map + +/***/ }), + +/***/ 605: +/***/ (function(module) { + +module.exports = require("http"); + +/***/ }), + +/***/ 618: +/***/ (function(module, __unusedexports, __webpack_require__) { + +/*! For license information please see mailgun.js.LICENSE.txt */ +!function(e,t){ true?module.exports=t():undefined}(this,(function(){return(()=>{var e={271:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(185);class n extends o.EventTarget{constructor(){throw super(),new TypeError("AbortSignal cannot be constructed directly")}get aborted(){const e=s.get(this);if("boolean"!=typeof e)throw new TypeError("Expected 'this' to be an 'AbortSignal' object, but got "+(null===this?"null":typeof this));return e}}o.defineEventAttribute(n.prototype,"abort");const s=new WeakMap;Object.defineProperties(n.prototype,{aborted:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(n.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortSignal"});class i{constructor(){a.set(this,function(){const e=Object.create(n.prototype);return o.EventTarget.call(e),s.set(e,!1),e}())}get signal(){return u(this)}abort(){var e;e=u(this),!1===s.get(e)&&(s.set(e,!0),e.dispatchEvent({type:"abort"}))}}const a=new WeakMap;function u(e){const t=a.get(e);if(null==t)throw new TypeError("Expected 'this' to be an 'AbortController' object, but got "+(null===e?"null":typeof e));return t}Object.defineProperties(i.prototype,{signal:{enumerable:!0},abort:{enumerable:!0}}),"function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag&&Object.defineProperty(i.prototype,Symbol.toStringTag,{configurable:!0,value:"AbortController"}),t.AbortController=i,t.AbortSignal=n,t.default=i,e.exports=i,e.exports.AbortController=e.exports.default=i,e.exports.AbortSignal=n},990:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(765)),s=function(){function e(e){this.formData=e}return e.prototype.client=function(e){return new n.default(e,this.formData)},e}();t.default=s},765:function(e,t,r){"use strict";var o=this&&this.__assign||function(){return(o=Object.assign||function(e){for(var t,r=1,o=arguments.length;r{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=r(78),n=(r(955),function(){function e(e){this.request=e}return e.prototype._parsePageNumber=function(e){return e.split("/").pop()},e.prototype._parsePage=function(e,t){return{id:e,number:this._parsePageNumber(t),url:t}},e.prototype._parsePageLinks=function(e){var t=this;return Object.entries(e.body.paging).reduce((function(e,r){var o=r[0],n=r[1];return e[o]=t._parsePage(o,n),e}),{})},e.prototype._parseEventList=function(e){return{items:e.body.items,pages:this._parsePageLinks(e)}},e.prototype.get=function(e,t){var r,n=this;return t&&t.page?(r=o("/v2",e,"events",t.page),delete t.page):r=o("/v2",e,"events"),this.request.get(r,t).then((function(e){return n._parseEventList(e)}))},e}());t.default=n},853:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),r(955);var o=function(){function e(e){this.request=e}return e.prototype.list=function(e){var t=this;return this.request.get("/v1/ip_pools",e).then((function(e){return t.parseIpPoolsResponse(e)}))},e.prototype.create=function(e){return this.request.post("/v1/ip_pools",e).then((function(e){return null==e?void 0:e.body}))},e.prototype.update=function(e,t){return this.request.patch("/v1/ip_pools/"+e,t).then((function(e){return null==e?void 0:e.body}))},e.prototype.delete=function(e,t){return this.request.delete("/v1/ip_pools/"+e,t).then((function(e){return null==e?void 0:e.body}))},e.prototype.parseIpPoolsResponse=function(e){return e.body.ip_pools},e}();t.default=o},580:(e,t,r)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),r(955);var o=function(){function e(e){this.request=e}return e.prototype.list=function(e){var t=this;return this.request.get("/v3/ips",e).then((function(e){return t.parseIpsResponse(e)}))},e.prototype.get=function(e){var t=this;return this.request.get("/v3/ips/"+e).then((function(e){return t.parseIpsResponse(e)}))},e.prototype.parseIpsResponse=function(e){return e.body},e}();t.default=o},616:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype._parseResponse=function(e){return e.body?e.body:e},e.prototype.create=function(e,t){return t.message?this.request.postMulti("/v3/"+e+"/messages.mime",t).then(this._parseResponse):this.request.postMulti("/v3/"+e+"/messages",t).then(this._parseResponse)},e}();t.default=r},726:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.get=function(e,t){var r={};return Array.isArray(e)&&(e=e.join(",")),r.addresses=e,t&&(r.syntax_only=!1),this.request.get("/v3/address/parse",r).then((function(e){return e.body}))},e}();t.default=r},955:function(e,t,r){"use strict";var o=this&&this.__assign||function(){return(o=Object.assign||function(e){for(var t,r=1,o=arguments.length;r0&&n[n.length-1])||6!==s[0]&&2!==s[0])){i=0;continue}if(3===s[0]&&(!n||s[1]>n[0]&&s[1]0&&(f.searchParams=r.query,delete f.query),[4,l.default(u.default(this.url,t),o({method:e.toLocaleUpperCase(),headers:i,throwHttpErrors:!1},f))];case 1:return(null==(p=s.sent())?void 0:p.ok)?[3,6]:(null==p?void 0:p.body)&&d(p.body)?[4,(m=p.body,_=[],new Promise((function(e,t){m.on("data",(function(e){return _.push(e)})),m.on("error",t),m.on("end",(function(){return e(Buffer.concat(_).toString("utf8"))}))})))]:[3,3];case 2:return y=s.sent(),[3,5];case 3:return[4,null==p?void 0:p.json()];case 4:y=s.sent(),s.label=5;case 5:throw h=y,new c.default({status:null==p?void 0:p.status,statusText:null==p?void 0:p.statusText,body:{message:h}});case 6:return b={},[4,null==p?void 0:p.json()];case 7:return[2,(b.body=s.sent(),b.status=null==p?void 0:p.status,b)]}var m,_}))}))},e.prototype.query=function(e,t,r,n){return this.request(e,t,o({query:r},n))},e.prototype.command=function(e,t,r,n){return this.request(e,t,o({headers:{"Content-Type":"application/x-www-form-urlencoded"},body:r},n))},e.prototype.get=function(e,t,r){return this.query("get",e,t,r)},e.prototype.head=function(e,t,r){return this.query("head",e,t,r)},e.prototype.options=function(e,t,r){return this.query("options",e,t,r)},e.prototype.post=function(e,t,r){return this.command("post",e,t,r)},e.prototype.postMulti=function(e,t){var r=new this.formData;return Object.keys(t).filter((function(e){return t[e]})).forEach((function(e){if("attachment"!==e)Array.isArray(t[e])?t[e].forEach((function(t){r.append(e,t)})):r.append(e,t[e]);else{var o=t.attachment;if(Array.isArray(o))o.forEach((function(t){var o=t.data?t.data:t,n=f(t);r.append(e,o,n)}));else{var n=d(o)?o:o.data,s=f(o);r.append(e,n,s)}}})),this.command("post",e,r,{headers:{"Content-Type":null}})},e.prototype.put=function(e,t,r){return this.command("put",e,t,r)},e.prototype.patch=function(e,t,r){return this.command("patch",e,t,r)},e.prototype.delete=function(e,t,r){return this.command("delete",e,t,r)},e}();t.default=p},893:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.list=function(e){return this.request.get("/v3/routes",e).then((function(e){return e.body.items}))},e.prototype.get=function(e){return this.request.get("/v3/routes/"+e).then((function(e){return e.body.route}))},e.prototype.create=function(e){return this.request.post("/v3/routes",e).then((function(e){return e.body.route}))},e.prototype.update=function(e,t){return this.request.put("/v3/routes/"+e,t).then((function(e){return e.body}))},e.prototype.destroy=function(e){return this.request.delete("/v3/routes/"+e).then((function(e){return e.body}))},e}();t.default=r},154:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(78)),s=function(e){this.start=new Date(e.start),this.end=new Date(e.end),this.resolution=e.resolution,this.stats=e.stats.map((function(e){return e.time=new Date(e.time),e}))},i=function(){function e(e){this.request=e}return e.prototype._parseStats=function(e){return new s(e.body)},e.prototype.getDomain=function(e,t){return this.request.get(n.default("/v3",e,"stats/total"),t).then(this._parseStats)},e.prototype.getAccount=function(e){return this.request.get("/v3/stats/total",e).then(this._parseStats)},e}();t.default=i},526:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(835)),s=o(r(78)),i={headers:{"Content-Type":"application/json"}},a=function(e){this.type="bounces",this.address=e.address,this.code=+e.code,this.error=e.error,this.created_at=new Date(e.created_at)},u=function(e){this.type="complaints",this.address=e.address,this.created_at=new Date(e.created_at)},l=function(e){this.type="unsubscribes",this.address=e.address,this.tags=e.tags,this.created_at=new Date(e.created_at)},c=function(){function e(e){this.request=e,this.models={bounces:a,complaints:u,unsubscribes:l}}return e.prototype._parsePage=function(e,t){var r=n.default.parse(t,!0).query;return{id:e,page:r.page,address:r.address,url:t}},e.prototype._parsePageLinks=function(e){var t=this;return Object.entries(e.body.paging).reduce((function(e,r){var o=r[0],n=r[1];return e[o]=t._parsePage(o,n),e}),{})},e.prototype._parseList=function(e,t){var r={};return r.items=e.body.items.map((function(e){return new t(e)})),r.pages=this._parsePageLinks(e),r},e.prototype._parseItem=function(e,t){return new t(e.body)},e.prototype.list=function(e,t,r){var o=this,n=this.models[t];return this.request.get(s.default("v3",e,t),r).then((function(e){return o._parseList(e,n)}))},e.prototype.get=function(e,t,r){var o=this,n=this.models[t];return this.request.get(s.default("v3",e,t,encodeURIComponent(r))).then((function(e){return o._parseItem(e,n)}))},e.prototype.create=function(e,t,r){return Array.isArray(r)||(r=[r]),this.request.post(s.default("v3",e,t),r,i).then((function(e){return e.body}))},e.prototype.destroy=function(e,t,r){return this.request.delete(s.default("v3",e,t,encodeURIComponent(r))).then((function(e){return e.body}))},e}();t.default=c,e.exports=c},335:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e){this.request=e}return e.prototype.get=function(e){return this.request.get("/v3/address/validate",{address:e}).then((function(e){return e.body}))},e}();t.default=r},632:function(e,t,r){"use strict";var o=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=o(r(78)),s=function(e,t){this.id=e,this.url=t.url},i=function(){function e(e){this.request=e}return e.prototype._parseWebhookList=function(e){return e.body.webhooks},e.prototype._parseWebhookWithID=function(e){return function(t){return new s(e,t.body.webhook)}},e.prototype._parseWebhookTest=function(e){return{code:e.body.code,message:e.body.message}},e.prototype.list=function(e,t){return this.request.get(n.default("/v2/domains",e,"webhooks"),t).then(this._parseWebhookList)},e.prototype.get=function(e,t){return this.request.get(n.default("/v2/domains",e,"webhooks",t)).then(this._parseWebhookWithID(t))},e.prototype.create=function(e,t,r,o){return o?this.request.put(n.default("/v2/domains",e,"webhooks",t,"test"),{url:r}).then(this._parseWebhookTest):this.request.post(n.default("/v2/domains",e,"webhooks"),{id:t,url:r}).then(this._parseWebhookWithID(t))},e.prototype.update=function(e,t,r){return this.request.put(n.default("/v2/domains",e,"webhooks",t),{url:r}).then(this._parseWebhookWithID(t))},e.prototype.destroy=function(e,t){return this.request.delete(n.default("/v2/domains",e,"webhooks",t)).then(this._parseWebhookWithID(t))},e}();t.default=i},706:e=>{!function(){"use strict";e.exports=function(e){return(e instanceof Buffer?e:Buffer.from(e.toString(),"binary")).toString("base64")}}()},175:e=>{"use strict";e.exports=function(e){if(!/^data:/i.test(e))throw new TypeError('`uri` does not appear to be a Data URI (must begin with "data:")');const t=(e=e.replace(/\r?\n/g,"")).indexOf(",");if(-1===t||t<=4)throw new TypeError("malformed data: URI");const r=e.substring(5,t).split(";");let o="",n=!1;const s=r[0]||"text/plain";let i=s;for(let e=1;e{"use strict";Object.defineProperty(t,"__esModule",{value:!0});const r=new WeakMap,o=new WeakMap;function n(e){const t=r.get(e);return console.assert(null!=t,"'this' is expected an Event object, but got",e),t}function s(e){null==e.passiveListener?e.event.cancelable&&(e.canceled=!0,"function"==typeof e.event.preventDefault&&e.event.preventDefault()):"undefined"!=typeof console&&"function"==typeof console.error&&console.error("Unable to preventDefault inside passive event listener invocation.",e.passiveListener)}function i(e,t){r.set(this,{eventTarget:e,event:t,eventPhase:2,currentTarget:e,canceled:!1,stopped:!1,immediateStopped:!1,passiveListener:null,timeStamp:t.timeStamp||Date.now()}),Object.defineProperty(this,"isTrusted",{value:!1,enumerable:!0});const o=Object.keys(t);for(let e=0;e0){const e=new Array(arguments.length);for(let t=0;t{const{Readable:o}=r(413),n=new WeakMap;class s{constructor(e=[],t={type:""}){let r=0;const o=e.map((e=>{let t;return t=e instanceof Buffer?e:ArrayBuffer.isView(e)?Buffer.from(e.buffer,e.byteOffset,e.byteLength):e instanceof ArrayBuffer?Buffer.from(e):e instanceof s?e:Buffer.from("string"==typeof e?e:String(e)),r+=t.length||t.size||0,t})),i=void 0===t.type?"":String(t.type).toLowerCase();n.set(this,{type:/[^\u0020-\u007E]/.test(i)?"":i,size:r,parts:o})}get size(){return n.get(this).size}get type(){return n.get(this).type}async text(){return Buffer.from(await this.arrayBuffer()).toString()}async arrayBuffer(){const e=new Uint8Array(this.size);let t=0;for await(const r of this.stream())e.set(r,t),t+=r.length;return e.buffer}stream(){return o.from(async function*(e){for(const t of e)"stream"in t?yield*t.stream():yield t}(n.get(this).parts))}slice(e=0,t=this.size,r=""){const{size:o}=this;let i=e<0?Math.max(o+e,0):Math.min(e,o),a=t<0?Math.max(o+t,0):Math.min(t,o);const u=Math.max(a-i,0),l=n.get(this).parts.values(),c=[];let d=0;for(const e of l){const t=ArrayBuffer.isView(e)?e.byteLength:e.size;if(i&&t<=i)i-=t,a-=t;else{const r=e.slice(i,Math.min(t,a));if(c.push(r),d+=ArrayBuffer.isView(r)?r.byteLength:r.size,i=0,d>=u)break}}const f=new s([],{type:r});return Object.assign(n.get(f),{size:u,parts:c}),f}get[Symbol.toStringTag](){return"Blob"}static[Symbol.hasInstance](e){return"object"==typeof e&&"function"==typeof e.stream&&0===e.stream.length&&"function"==typeof e.constructor&&/^(Blob|File)$/.test(e[Symbol.toStringTag])}}Object.defineProperties(s.prototype,{size:{enumerable:!0},type:{enumerable:!0},slice:{enumerable:!0}}),e.exports=s},556:(e,t,r)=>{"use strict";const o=r(711),n=r(271);if(global.fetch||(global.fetch=(e,t)=>o(e,{highWaterMark:1e7,...t})),global.Headers||(global.Headers=o.Headers),global.Request||(global.Request=o.Request),global.Response||(global.Response=o.Response),global.AbortController||(global.AbortController=n),!global.ReadableStream)try{global.ReadableStream=r(377)}catch(e){}e.exports=r(721)},721:function(e){var t;t=function(){"use strict";const e={},t=e=>"undefined"!=typeof self&&self&&e in self?self:"undefined"!=typeof window&&window&&e in window?window:"undefined"!=typeof global&&global&&e in global?global:"undefined"!=typeof globalThis&&globalThis?globalThis:void 0,r=["Headers","Request","Response","ReadableStream","fetch","AbortController","FormData"];for(const o of r)Object.defineProperty(e,o,{get(){const e=t(o),r=e&&e[o];return"function"==typeof r?r.bind(e):r}});const o=e=>null!==e&&"object"==typeof e,n="function"==typeof e.AbortController,s="function"==typeof e.ReadableStream,i="function"==typeof e.FormData,a=(t,r)=>{const o=new e.Headers(t||{}),n=r instanceof e.Headers,s=new e.Headers(r||{});for(const[e,t]of s)n&&"undefined"===t||void 0===t?o.delete(e):o.set(e,t);return o},u=(...e)=>{let t={},r={};for(const n of e){if(Array.isArray(n))Array.isArray(t)||(t=[]),t=[...t,...n];else if(o(n)){for(let[e,r]of Object.entries(n))o(r)&&e in t&&(r=u(t[e],r)),t={...t,[e]:r};o(n.headers)&&(r=a(r,n.headers))}t.headers=r}return t},l=["get","post","put","patch","head","delete"],c={json:"application/json",text:"text/*",formData:"multipart/form-data",arrayBuffer:"*/*",blob:"*/*"},d=[413,429,503],f=Symbol("stop");class p extends Error{constructor(e){super(e.statusText||String(0===e.status||e.status?e.status:"Unknown response error")),this.name="HTTPError",this.response=e}}class h extends Error{constructor(e){super("Request timed out"),this.name="TimeoutError",this.request=e}}const y=e=>new Promise((t=>setTimeout(t,e))),b=e=>l.includes(e)?e.toUpperCase():e,m={limit:2,methods:["get","put","head","delete","options","trace"],statusCodes:[408,413,429,500,502,503,504],afterStatusCodes:d},_=(e={})=>{if("number"==typeof e)return{...m,limit:e};if(e.methods&&!Array.isArray(e.methods))throw new Error("retry.methods must be an array");if(e.statusCodes&&!Array.isArray(e.statusCodes))throw new Error("retry.statusCodes must be an array");return{...m,...e,afterStatusCodes:d}},g=2147483647;class w{constructor(t,r={}){if(this._retryCount=0,this._input=t,this._options={credentials:this._input.credentials||"same-origin",...r,headers:a(this._input.headers,r.headers),hooks:u({beforeRequest:[],beforeRetry:[],afterResponse:[]},r.hooks),method:b(r.method||this._input.method),prefixUrl:String(r.prefixUrl||""),retry:_(r.retry),throwHttpErrors:!1!==r.throwHttpErrors,timeout:void 0===r.timeout?1e4:r.timeout,fetch:r.fetch||e.fetch},"string"!=typeof this._input&&!(this._input instanceof URL||this._input instanceof e.Request))throw new TypeError("`input` must be a string, URL, or Request");if(this._options.prefixUrl&&"string"==typeof this._input){if(this._input.startsWith("/"))throw new Error("`input` must not begin with a slash when using `prefixUrl`");this._options.prefixUrl.endsWith("/")||(this._options.prefixUrl+="/"),this._input=this._options.prefixUrl+this._input}if(n&&(this.abortController=new e.AbortController,this._options.signal&&this._options.signal.addEventListener("abort",(()=>{this.abortController.abort()})),this._options.signal=this.abortController.signal),this.request=new e.Request(this._input,this._options),this._options.searchParams){const t="?"+new URLSearchParams(this._options.searchParams).toString(),r=this.request.url.replace(/(?:\?.*?)?(?=#|$)/,t);!(i&&this._options.body instanceof e.FormData||this._options.body instanceof URLSearchParams)||this._options.headers&&this._options.headers["content-type"]||this.request.headers.delete("content-type"),this.request=new e.Request(new e.Request(r,this.request),this._options)}void 0!==this._options.json&&(this._options.body=JSON.stringify(this._options.json),this.request.headers.set("content-type","application/json"),this.request=new e.Request(this.request,{body:this._options.body}));const o=async()=>{if(this._options.timeout>g)throw new RangeError("The `timeout` option cannot be greater than 2147483647");await y(1);let t=await this._fetch();for(const r of this._options.hooks.afterResponse){const o=await r(this.request,this._options,this._decorateResponse(t.clone()));o instanceof e.Response&&(t=o)}if(this._decorateResponse(t),!t.ok&&this._options.throwHttpErrors)throw new p(t);if(this._options.onDownloadProgress){if("function"!=typeof this._options.onDownloadProgress)throw new TypeError("The `onDownloadProgress` option must be a function");if(!s)throw new Error("Streams are not supported in your environment. `ReadableStream` is missing.");return this._stream(t.clone(),this._options.onDownloadProgress)}return t},l=this._options.retry.methods.includes(this.request.method.toLowerCase())?this._retry(o):o();for(const[e,t]of Object.entries(c))l[e]=async()=>{this.request.headers.set("accept",this.request.headers.get("accept")||t);const o=(await l).clone();if("json"===e){if(204===o.status)return"";if(r.parseJson)return r.parseJson(await o.text())}return o[e]()};return l}_calculateRetryDelay(e){if(this._retryCount++,this._retryCountthis._options.retry.maxRetryAfter?0:e}if(413===e.response.status)return 0}return.3*2**(this._retryCount-1)*1e3}return 0}_decorateResponse(e){return this._options.parseJson&&(e.json=async()=>this._options.parseJson(await e.text())),e}async _retry(e){try{return await e()}catch(t){const r=Math.min(this._calculateRetryDelay(t),g);if(0!==r&&this._retryCount>0){await y(r);for(const e of this._options.hooks.beforeRetry)if(await e({request:this.request,options:this._options,error:t,retryCount:this._retryCount})===f)return;return this._retry(e)}if(this._options.throwHttpErrors)throw t}}async _fetch(){for(const e of this._options.hooks.beforeRequest){const t=await e(this.request,this._options);if(t instanceof Request){this.request=t;break}if(t instanceof Response)return t}return!1===this._options.timeout?this._options.fetch(this.request.clone()):(e=this.request.clone(),t=this.abortController,r=this._options,new Promise(((o,n)=>{const s=setTimeout((()=>{t&&t.abort(),n(new h(e))}),r.timeout);r.fetch(e).then(o).catch(n).then((()=>{clearTimeout(s)}))})));var e,t,r}_stream(t,r){const o=Number(t.headers.get("content-length"))||0;let n=0;return new e.Response(new e.ReadableStream({start(e){const s=t.body.getReader();r&&r({percent:0,transferredBytes:0,totalBytes:o},new Uint8Array),async function t(){const{done:i,value:a}=await s.read();i?e.close():(r&&(n+=a.byteLength,r({percent:0===o?0:n/o,transferredBytes:n,totalBytes:o},a)),e.enqueue(a),t())}()}}))}}const v=(...e)=>{for(const t of e)if((!o(t)||Array.isArray(t))&&void 0!==t)throw new TypeError("The `options` argument must be an object");return u({},...e)},S=e=>{const t=(t,r)=>new w(t,v(e,r));for(const r of l)t[r]=(t,o)=>new w(t,v(e,o,{method:r}));return t.HTTPError=p,t.TimeoutError=h,t.create=e=>S(v(e)),t.extend=t=>S(v(e,t)),t.stop=f,t};return S()},e.exports=t()},711:(e,t,r)=>{"use strict";t=e.exports=I;const o=r(605),n=r(211),s=r(761),i=r(413),a=r(175),u=r(669),l=r(30),c=r(417),d=r(835);class f extends Error{constructor(e,t){super(e),Error.captureStackTrace(this,this.constructor),this.type=t}get name(){return this.constructor.name}get[Symbol.toStringTag](){return this.constructor.name}}class p extends f{constructor(e,t,r){super(e,t),r&&(this.code=this.errno=r.code,this.erroredSysCall=r.syscall)}}const h=Symbol.toStringTag,y=e=>"object"==typeof e&&"function"==typeof e.append&&"function"==typeof e.delete&&"function"==typeof e.get&&"function"==typeof e.getAll&&"function"==typeof e.has&&"function"==typeof e.set&&"function"==typeof e.sort&&"URLSearchParams"===e[h],b=e=>"object"==typeof e&&"function"==typeof e.arrayBuffer&&"string"==typeof e.type&&"function"==typeof e.stream&&"function"==typeof e.constructor&&/^(Blob|File)$/.test(e[h]);function m(e){return"object"==typeof e&&"function"==typeof e.append&&"function"==typeof e.set&&"function"==typeof e.get&&"function"==typeof e.getAll&&"function"==typeof e.delete&&"function"==typeof e.keys&&"function"==typeof e.values&&"function"==typeof e.entries&&"function"==typeof e.constructor&&"FormData"===e[h]}const _="\r\n",g="-".repeat(2),w=Buffer.byteLength(_),v=e=>`${g}${e}${g}${_.repeat(2)}`;function S(e,t,r){let o="";return o+=`${g}${e}\r\n`,o+=`Content-Disposition: form-data; name="${t}"`,b(r)&&(o+=`; filename="${r.name}"\r\n`,o+=`Content-Type: ${r.type||"application/octet-stream"}`),`${o}${_.repeat(2)}`}const T=Symbol("Body internals");class R{constructor(e,{size:t=0}={}){let r=null;null===e?e=null:y(e)?e=Buffer.from(e.toString()):b(e)||Buffer.isBuffer(e)||(u.types.isAnyArrayBuffer(e)?e=Buffer.from(e):ArrayBuffer.isView(e)?e=Buffer.from(e.buffer,e.byteOffset,e.byteLength):e instanceof i||(m(e)?(r=`NodeFetchFormDataBoundary${c.randomBytes(8).toString("hex")}`,e=i.Readable.from(async function*(e,t){for(const[r,o]of e)yield S(t,r,o),b(o)?yield*o.stream():yield o,yield _;yield v(t)}(e,r))):e=Buffer.from(String(e)))),this[T]={body:e,boundary:r,disturbed:!1,error:null},this.size=t,e instanceof i&&e.on("error",(e=>{const t=e instanceof f?e:new p(`Invalid response body while trying to fetch ${this.url}: ${e.message}`,"system",e);this[T].error=t}))}get body(){return this[T].body}get bodyUsed(){return this[T].disturbed}async arrayBuffer(){const{buffer:e,byteOffset:t,byteLength:r}=await q(this);return e.slice(t,t+r)}async blob(){const e=this.headers&&this.headers.get("content-type")||this[T].body&&this[T].body.type||"",t=await this.buffer();return new l([t],{type:e})}async json(){const e=await q(this);return JSON.parse(e.toString())}async text(){return(await q(this)).toString()}buffer(){return q(this)}}async function q(e){if(e[T].disturbed)throw new TypeError(`body used already for: ${e.url}`);if(e[T].disturbed=!0,e[T].error)throw e[T].error;let{body:t}=e;if(null===t)return Buffer.alloc(0);if(b(t)&&(t=t.stream()),Buffer.isBuffer(t))return t;if(!(t instanceof i))return Buffer.alloc(0);const r=[];let o=0;try{for await(const n of t){if(e.size>0&&o+n.length>e.size){const r=new p(`content size at ${e.url} over limit: ${e.size}`,"max-size");throw t.destroy(r),r}o+=n.length,r.push(n)}}catch(t){throw t instanceof f?t:new p(`Invalid response body while trying to fetch ${e.url}: ${t.message}`,"system",t)}if(!0!==t.readableEnded&&!0!==t._readableState.ended)throw new p(`Premature close of server response while trying to fetch ${e.url}`);try{return r.every((e=>"string"==typeof e))?Buffer.from(r.join("")):Buffer.concat(r,o)}catch(t){throw new p(`Could not create Buffer from response body for ${e.url}: ${t.message}`,"system",t)}}Object.defineProperties(R.prototype,{body:{enumerable:!0},bodyUsed:{enumerable:!0},arrayBuffer:{enumerable:!0},blob:{enumerable:!0},json:{enumerable:!0},text:{enumerable:!0}});const P=(e,t)=>{let r,o,{body:n}=e;if(e.bodyUsed)throw new Error("cannot clone body after it is used");return n instanceof i&&"function"!=typeof n.getBoundary&&(r=new i.PassThrough({highWaterMark:t}),o=new i.PassThrough({highWaterMark:t}),n.pipe(r),n.pipe(o),e[T].body=r,n=o),n},E=(e,t)=>null===e?null:"string"==typeof e?"text/plain;charset=UTF-8":y(e)?"application/x-www-form-urlencoded;charset=UTF-8":b(e)?e.type||null:Buffer.isBuffer(e)||u.types.isAnyArrayBuffer(e)||ArrayBuffer.isView(e)?null:e&&"function"==typeof e.getBoundary?`multipart/form-data;boundary=${e.getBoundary()}`:m(e)?`multipart/form-data; boundary=${t[T].boundary}`:e instanceof i?null:"text/plain;charset=UTF-8",j="function"==typeof o.validateHeaderName?o.validateHeaderName:e=>{if(!/^[\^`\-\w!#$%&'*+.|~]+$/.test(e)){const t=new TypeError(`Header name must be a valid HTTP token [${e}]`);throw Object.defineProperty(t,"code",{value:"ERR_INVALID_HTTP_TOKEN"}),t}},k="function"==typeof o.validateHeaderValue?o.validateHeaderValue:(e,t)=>{if(/[^\t\u0020-\u007E\u0080-\u00FF]/.test(t)){const t=new TypeError(`Invalid character in header content ["${e}"]`);throw Object.defineProperty(t,"code",{value:"ERR_INVALID_CHAR"}),t}};class O extends URLSearchParams{constructor(e){let t=[];if(e instanceof O){const r=e.raw();for(const[e,o]of Object.entries(r))t.push(...o.map((t=>[e,t])))}else if(null==e);else{if("object"!=typeof e||u.types.isBoxedPrimitive(e))throw new TypeError("Failed to construct 'Headers': The provided value is not of type '(sequence> or record)");{const r=e[Symbol.iterator];if(null==r)t.push(...Object.entries(e));else{if("function"!=typeof r)throw new TypeError("Header pairs must be iterable");t=[...e].map((e=>{if("object"!=typeof e||u.types.isBoxedPrimitive(e))throw new TypeError("Each header pair must be an iterable object");return[...e]})).map((e=>{if(2!==e.length)throw new TypeError("Each header pair must be a name/value tuple");return[...e]}))}}}return t=t.length>0?t.map((([e,t])=>(j(e),k(e,String(t)),[String(e).toLowerCase(),String(t)]))):void 0,super(t),new Proxy(this,{get(e,t,r){switch(t){case"append":case"set":return(e,o)=>(j(e),k(e,String(o)),URLSearchParams.prototype[t].call(r,String(e).toLowerCase(),String(o)));case"delete":case"has":case"getAll":return e=>(j(e),URLSearchParams.prototype[t].call(r,String(e).toLowerCase()));case"keys":return()=>(e.sort(),new Set(URLSearchParams.prototype.keys.call(e)).keys());default:return Reflect.get(e,t,r)}}})}get[Symbol.toStringTag](){return this.constructor.name}toString(){return Object.prototype.toString.call(this)}get(e){const t=this.getAll(e);if(0===t.length)return null;let r=t.join(", ");return/^content-encoding$/i.test(e)&&(r=r.toLowerCase()),r}forEach(e){for(const t of this.keys())e(this.get(t),t)}*values(){for(const e of this.keys())yield this.get(e)}*entries(){for(const e of this.keys())yield[e,this.get(e)]}[Symbol.iterator](){return this.entries()}raw(){return[...this.keys()].reduce(((e,t)=>(e[t]=this.getAll(t),e)),{})}[Symbol.for("nodejs.util.inspect.custom")](){return[...this.keys()].reduce(((e,t)=>{const r=this.getAll(t);return e[t]="host"===t?r[0]:r.length>1?r:r[0],e}),{})}}Object.defineProperties(O.prototype,["get","entries","forEach","values"].reduce(((e,t)=>(e[t]={enumerable:!0},e)),{}));const C=new Set([301,302,303,307,308]),A=e=>C.has(e),B=Symbol("Response internals");class x extends R{constructor(e=null,t={}){super(e,t);const r=t.status||200,o=new O(t.headers);if(null!==e&&!o.has("Content-Type")){const t=E(e);t&&o.append("Content-Type",t)}this[B]={url:t.url,status:r,statusText:t.statusText||"",headers:o,counter:t.counter,highWaterMark:t.highWaterMark}}get url(){return this[B].url||""}get status(){return this[B].status}get ok(){return this[B].status>=200&&this[B].status<300}get redirected(){return this[B].counter>0}get statusText(){return this[B].statusText}get headers(){return this[B].headers}get highWaterMark(){return this[B].highWaterMark}clone(){return new x(P(this,this.highWaterMark),{url:this.url,status:this.status,statusText:this.statusText,headers:this.headers,ok:this.ok,redirected:this.redirected,size:this.size})}static redirect(e,t=302){if(!A(t))throw new RangeError('Failed to execute "redirect" on "response": Invalid status code');return new x(null,{headers:{location:new URL(e).toString()},status:t})}get[Symbol.toStringTag](){return"Response"}}Object.defineProperties(x.prototype,{url:{enumerable:!0},status:{enumerable:!0},ok:{enumerable:!0},redirected:{enumerable:!0},statusText:{enumerable:!0},headers:{enumerable:!0},clone:{enumerable:!0}});const W=Symbol("Request internals"),L=e=>"object"==typeof e&&"object"==typeof e[W];class z extends R{constructor(e,t={}){let r;L(e)?r=new URL(e.url):(r=new URL(e),e={});let o=t.method||e.method||"GET";if(o=o.toUpperCase(),(null!=t.body||L(e))&&null!==e.body&&("GET"===o||"HEAD"===o))throw new TypeError("Request with GET/HEAD method cannot have body");const n=t.body?t.body:L(e)&&null!==e.body?P(e):null;super(n,{size:t.size||e.size||0});const s=new O(t.headers||e.headers||{});if(null!==n&&!s.has("Content-Type")){const e=E(n,this);e&&s.append("Content-Type",e)}let i=L(e)?e.signal:null;if("signal"in t&&(i=t.signal),null!==i&&("object"!=typeof(a=i)||"AbortSignal"!==a[h]))throw new TypeError("Expected signal to be an instanceof AbortSignal");var a;this[W]={method:o,redirect:t.redirect||e.redirect||"follow",headers:s,parsedURL:r,signal:i},this.follow=void 0===t.follow?void 0===e.follow?20:e.follow:t.follow,this.compress=void 0===t.compress?void 0===e.compress||e.compress:t.compress,this.counter=t.counter||e.counter||0,this.agent=t.agent||e.agent,this.highWaterMark=t.highWaterMark||e.highWaterMark||16384,this.insecureHTTPParser=t.insecureHTTPParser||e.insecureHTTPParser||!1}get method(){return this[W].method}get url(){return d.format(this[W].parsedURL)}get headers(){return this[W].headers}get redirect(){return this[W].redirect}get signal(){return this[W].signal}clone(){return new z(this)}get[Symbol.toStringTag](){return"Request"}}Object.defineProperties(z.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0}});class M extends f{constructor(e,t="aborted"){super(e,t)}}const $=new Set(["data:","http:","https:"]);async function I(e,t){return new Promise(((r,u)=>{const l=new z(e,t),c=(e=>{const{parsedURL:t}=e[W],r=new O(e[W].headers);r.has("Accept")||r.set("Accept","*/*");let o=null;if(null===e.body&&/^(post|put)$/i.test(e.method)&&(o="0"),null!==e.body){const t=(e=>{const{body:t}=e;return null===t?0:b(t)?t.size:Buffer.isBuffer(t)?t.length:t&&"function"==typeof t.getLengthSync?t.hasKnownLength&&t.hasKnownLength()?t.getLengthSync():null:m(t)?function(e,t){let r=0;for(const[o,n]of e)r+=Buffer.byteLength(S(t,o,n)),b(n)?r+=n.size:r+=Buffer.byteLength(String(n)),r+=w;return r+=Buffer.byteLength(v(t)),r}(e[T].boundary):null})(e);"number"!=typeof t||Number.isNaN(t)||(o=String(t))}o&&r.set("Content-Length",o),r.has("User-Agent")||r.set("User-Agent","node-fetch"),e.compress&&!r.has("Accept-Encoding")&&r.set("Accept-Encoding","gzip,deflate,br");let{agent:n}=e;"function"==typeof n&&(n=n(t)),r.has("Connection")||n||r.set("Connection","close");const s=(e=>{if(e.search)return e.search;const t=e.href.length-1,r=e.hash||("#"===e.href[t]?"#":"");return"?"===e.href[t-r.length]?"?":""})(t);return{path:t.pathname+s,pathname:t.pathname,hostname:t.hostname,protocol:t.protocol,port:t.port,hash:t.hash,search:t.search,query:t.query,href:t.href,method:e.method,headers:r[Symbol.for("nodejs.util.inspect.custom")](),insecureHTTPParser:e.insecureHTTPParser,agent:n}})(l);if(!$.has(c.protocol))throw new TypeError(`node-fetch cannot load ${e}. URL scheme "${c.protocol.replace(/:$/,"")}" is not supported.`);if("data:"===c.protocol){const e=a(l.url),t=new x(e,{headers:{"Content-Type":e.typeFull}});return void r(t)}const d=("https:"===c.protocol?n:o).request,{signal:f}=l;let h=null;const y=()=>{const e=new M("The operation was aborted.");u(e),l.body&&l.body instanceof i.Readable&&l.body.destroy(e),h&&h.body&&h.body.emit("error",e)};if(f&&f.aborted)return void y();const _=()=>{y(),R()},g=d(c);f&&f.addEventListener("abort",_);const R=()=>{g.abort(),f&&f.removeEventListener("abort",_)};g.on("error",(e=>{u(new p(`request to ${l.url} failed, reason: ${e.message}`,"system",e)),R()})),g.on("response",(e=>{g.setTimeout(0);const o=function(e=[]){return new O(e.reduce(((e,t,r,o)=>(r%2==0&&e.push(o.slice(r,r+2)),e)),[]).filter((([e,t])=>{try{return j(e),k(e,String(t)),!0}catch{return!1}})))}(e.rawHeaders);if(A(e.statusCode)){const n=o.get("Location"),s=null===n?null:new URL(n,l.url);switch(l.redirect){case"error":return u(new p(`uri requested responds with a redirect, redirect mode is set to error: ${l.url}`,"no-redirect")),void R();case"manual":if(null!==s)try{o.set("Location",s)}catch(e){u(e)}break;case"follow":{if(null===s)break;if(l.counter>=l.follow)return u(new p(`maximum redirect reached at: ${l.url}`,"max-redirect")),void R();const o={headers:new O(l.headers),follow:l.follow,counter:l.counter+1,agent:l.agent,compress:l.compress,method:l.method,body:l.body,signal:l.signal,size:l.size};return 303!==e.statusCode&&l.body&&t.body instanceof i.Readable?(u(new p("Cannot follow redirect with body being a readable stream","unsupported-redirect")),void R()):(303!==e.statusCode&&(301!==e.statusCode&&302!==e.statusCode||"POST"!==l.method)||(o.method="GET",o.body=void 0,o.headers.delete("content-length")),r(I(new z(s,o))),void R())}}}e.once("end",(()=>{f&&f.removeEventListener("abort",_)}));let n=i.pipeline(e,new i.PassThrough,(e=>{u(e)}));process.version<"v12.10"&&e.on("aborted",_);const a={url:l.url,status:e.statusCode,statusText:e.statusMessage,headers:o,size:l.size,counter:l.counter,highWaterMark:l.highWaterMark},c=o.get("Content-Encoding");if(!l.compress||"HEAD"===l.method||null===c||204===e.statusCode||304===e.statusCode)return h=new x(n,a),void r(h);const d={flush:s.Z_SYNC_FLUSH,finishFlush:s.Z_SYNC_FLUSH};if("gzip"===c||"x-gzip"===c)return n=i.pipeline(n,s.createGunzip(d),(e=>{u(e)})),h=new x(n,a),void r(h);if("deflate"!==c&&"x-deflate"!==c){if("br"===c)return n=i.pipeline(n,s.createBrotliDecompress(),(e=>{u(e)})),h=new x(n,a),void r(h);h=new x(n,a),r(h)}else i.pipeline(e,new i.PassThrough,(e=>{u(e)})).once("data",(e=>{n=8==(15&e[0])?i.pipeline(n,s.createInflate(),(e=>{u(e)})):i.pipeline(n,s.createInflateRaw(),(e=>{u(e)})),h=new x(n,a),r(h)}))})),((e,{body:t})=>{null===t?e.end():b(t)?t.stream().pipe(e):Buffer.isBuffer(t)?(e.write(t),e.end()):t.pipe(e)})(g,l)}))}t.AbortError=M,t.FetchError=p,t.Headers=O,t.Request=z,t.Response=x,t.default=I,t.isRedirect=A},78:e=>{function t(e){return e.replace(/[\/]+/g,"/").replace(/\/\?/g,"?").replace(/\/\#/g,"#").replace(/\:\//g,"://")}e.exports=function(){var e=[].slice.call(arguments,0).join("/");return t(e)}},377:(e,t,r)=>{"use strict";r.r(t),r.d(t,{ByteLengthQueuingStrategy:()=>pr,CountQueuingStrategy:()=>mr,ReadableByteStreamController:()=>ye,ReadableStream:()=>rr,ReadableStreamBYOBReader:()=>ze,ReadableStreamBYOBRequest:()=>he,ReadableStreamDefaultController:()=>Mt,ReadableStreamDefaultReader:()=>X,TransformStream:()=>Tr,TransformStreamDefaultController:()=>jr,WritableStream:()=>Ge,WritableStreamDefaultController:()=>yt,WritableStreamDefaultWriter:()=>ut});const o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol:e=>`Symbol(${e})`;function n(){}const s="undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:void 0;function i(e){return"object"==typeof e&&null!==e||"function"==typeof e}const a=n,u=Promise,l=Promise.prototype.then,c=Promise.resolve.bind(u),d=Promise.reject.bind(u);function f(e){return new u(e)}function p(e){return c(e)}function h(e){return d(e)}function y(e,t,r){return l.call(e,t,r)}function b(e,t,r){y(y(e,t,r),void 0,a)}function m(e,t){b(e,t)}function _(e,t){b(e,void 0,t)}function g(e,t,r){return y(e,t,r)}function w(e){y(e,void 0,a)}const v=(()=>{const e=s&&s.queueMicrotask;if("function"==typeof e)return e;const t=p(void 0);return e=>y(t,e)})();function S(e,t,r){if("function"!=typeof e)throw new TypeError("Argument is not a function");return Function.prototype.apply.call(e,t,r)}function T(e,t,r){try{return p(S(e,t,r))}catch(e){return h(e)}}class R{constructor(){this._cursor=0,this._size=0,this._front={_elements:[],_next:void 0},this._back=this._front,this._cursor=0,this._size=0}get length(){return this._size}push(e){const t=this._back;let r=t;16383===t._elements.length&&(r={_elements:[],_next:void 0}),t._elements.push(e),r!==t&&(this._back=r,t._next=r),++this._size}shift(){const e=this._front;let t=e;const r=this._cursor;let o=r+1;const n=e._elements,s=n[r];return 16384===o&&(t=e._next,o=0),--this._size,this._cursor=o,e!==t&&(this._front=t),n[r]=void 0,s}forEach(e){let t=this._cursor,r=this._front,o=r._elements;for(;!(t===o.length&&void 0===r._next||t===o.length&&(r=r._next,o=r._elements,t=0,0===o.length));)e(o[t]),++t}peek(){const e=this._front,t=this._cursor;return e._elements[t]}}function q(e,t){e._ownerReadableStream=t,t._reader=e,"readable"===t._state?k(e):"closed"===t._state?function(e){k(e),A(e)}(e):O(e,t._storedError)}function P(e,t){return ar(e._ownerReadableStream,t)}function E(e){"readable"===e._ownerReadableStream._state?C(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness")):function(e,t){O(e,new TypeError("Reader was released and can no longer be used to monitor the stream's closedness"))}(e),e._ownerReadableStream._reader=void 0,e._ownerReadableStream=void 0}function j(e){return new TypeError("Cannot "+e+" a stream using a released reader")}function k(e){e._closedPromise=f(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r}))}function O(e,t){k(e),C(e,t)}function C(e,t){void 0!==e._closedPromise_reject&&(w(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}function A(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0)}const B=o("[[AbortSteps]]"),x=o("[[ErrorSteps]]"),W=o("[[CancelSteps]]"),L=o("[[PullSteps]]"),z=Number.isFinite||function(e){return"number"==typeof e&&isFinite(e)},M=Math.trunc||function(e){return e<0?Math.ceil(e):Math.floor(e)};function $(e,t){if(void 0!==e&&"object"!=typeof(r=e)&&"function"!=typeof r)throw new TypeError(`${t} is not an object.`);var r}function I(e,t){if("function"!=typeof e)throw new TypeError(`${t} is not a function.`)}function D(e,t){if(!function(e){return"object"==typeof e&&null!==e||"function"==typeof e}(e))throw new TypeError(`${t} is not an object.`)}function F(e,t,r){if(void 0===e)throw new TypeError(`Parameter ${t} is required in '${r}'.`)}function U(e,t,r){if(void 0===e)throw new TypeError(`${t} is required in '${r}'.`)}function H(e){return Number(e)}function N(e){return 0===e?0:e}function V(e,t){const r=Number.MAX_SAFE_INTEGER;let o=Number(e);if(o=N(o),!z(o))throw new TypeError(`${t} is not a finite number`);if(o=function(e){return N(M(e))}(o),o<0||o>r)throw new TypeError(`${t} is outside the accepted range of 0 to ${r}, inclusive`);return z(o)&&0!==o?o:0}function Q(e,t){if(!sr(e))throw new TypeError(`${t} is not a ReadableStream.`)}function Y(e){return new X(e)}function G(e,t){e._reader._readRequests.push(t)}function J(e,t,r){const o=e._reader._readRequests.shift();r?o._closeSteps():o._chunkSteps(t)}function K(e){return e._reader._readRequests.length}function Z(e){const t=e._reader;return void 0!==t&&!!ee(t)}class X{constructor(e){if(F(e,1,"ReadableStreamDefaultReader"),Q(e,"First parameter"),ir(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");q(this,e),this._readRequests=new R}get closed(){return ee(this)?this._closedPromise:h(re("closed"))}cancel(e){return ee(this)?void 0===this._ownerReadableStream?h(j("cancel")):P(this,e):h(re("cancel"))}read(){if(!ee(this))return h(re("read"));if(void 0===this._ownerReadableStream)return h(j("read from"));let e,t;const r=f(((r,o)=>{e=r,t=o}));return te(this,{_chunkSteps:t=>e({value:t,done:!1}),_closeSteps:()=>e({value:void 0,done:!0}),_errorSteps:e=>t(e)}),r}releaseLock(){if(!ee(this))throw re("releaseLock");if(void 0!==this._ownerReadableStream){if(this._readRequests.length>0)throw new TypeError("Tried to release a reader lock when that reader has pending read() calls un-settled");E(this)}}}function ee(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readRequests")}function te(e,t){const r=e._ownerReadableStream;r._disturbed=!0,"closed"===r._state?t._closeSteps():"errored"===r._state?t._errorSteps(r._storedError):r._readableStreamController[L](t)}function re(e){return new TypeError(`ReadableStreamDefaultReader.prototype.${e} can only be used on a ReadableStreamDefaultReader`)}Object.defineProperties(X.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(X.prototype,o.toStringTag,{value:"ReadableStreamDefaultReader",configurable:!0});const oe=Object.getPrototypeOf(Object.getPrototypeOf((async function*(){})).prototype);class ne{constructor(e,t){this._ongoingPromise=void 0,this._isFinished=!1,this._reader=e,this._preventCancel=t}next(){const e=()=>this._nextSteps();return this._ongoingPromise=this._ongoingPromise?g(this._ongoingPromise,e,e):e(),this._ongoingPromise}return(e){const t=()=>this._returnSteps(e);return this._ongoingPromise?g(this._ongoingPromise,t,t):t()}_nextSteps(){if(this._isFinished)return Promise.resolve({value:void 0,done:!0});const e=this._reader;if(void 0===e._ownerReadableStream)return h(j("iterate"));let t,r;const o=f(((e,o)=>{t=e,r=o}));return te(e,{_chunkSteps:e=>{this._ongoingPromise=void 0,v((()=>t({value:e,done:!1})))},_closeSteps:()=>{this._ongoingPromise=void 0,this._isFinished=!0,E(e),t({value:void 0,done:!0})},_errorSteps:t=>{this._ongoingPromise=void 0,this._isFinished=!0,E(e),r(t)}}),o}_returnSteps(e){if(this._isFinished)return Promise.resolve({value:e,done:!0});this._isFinished=!0;const t=this._reader;if(void 0===t._ownerReadableStream)return h(j("finish iterating"));if(!this._preventCancel){const r=P(t,e);return E(t),g(r,(()=>({value:e,done:!0})))}return E(t),p({value:e,done:!0})}}const se={next(){return ie(this)?this._asyncIteratorImpl.next():h(ae("next"))},return(e){return ie(this)?this._asyncIteratorImpl.return(e):h(ae("return"))}};function ie(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_asyncIteratorImpl")}function ae(e){return new TypeError(`ReadableStreamAsyncIterator.${e} can only be used on a ReadableSteamAsyncIterator`)}void 0!==oe&&Object.setPrototypeOf(se,oe);const ue=Number.isNaN||function(e){return e!=e};function le(e){return!!function(e){return"number"==typeof e&&(!ue(e)&&!(e<0))}(e)&&e!==1/0}function ce(e){const t=e._queue.shift();return e._queueTotalSize-=t.size,e._queueTotalSize<0&&(e._queueTotalSize=0),t.value}function de(e,t,r){if(!le(r=Number(r)))throw new RangeError("Size must be a finite, non-NaN, non-negative number.");e._queue.push({value:t,size:r}),e._queueTotalSize+=r}function fe(e){e._queue=new R,e._queueTotalSize=0}function pe(e){return e.slice()}class he{constructor(){throw new TypeError("Illegal constructor")}get view(){if(!me(this))throw Ae("view");return this._view}respond(e){if(!me(this))throw Ae("respond");if(F(e,1,"respond"),e=V(e,"First parameter"),void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");this._view.buffer,function(e,t){if(!le(t=Number(t)))throw new RangeError("bytesWritten must be a finite");Ee(e,t)}(this._associatedReadableByteStreamController,e)}respondWithNewView(e){if(!me(this))throw Ae("respondWithNewView");if(F(e,1,"respondWithNewView"),!ArrayBuffer.isView(e))throw new TypeError("You can only respond with array buffer views");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(void 0===this._associatedReadableByteStreamController)throw new TypeError("This BYOB request has been invalidated");!function(e,t){const r=e._pendingPullIntos.peek();if(r.byteOffset+r.bytesFilled!==t.byteOffset)throw new RangeError("The region specified by view does not match byobRequest");if(r.byteLength!==t.byteLength)throw new RangeError("The buffer of view has different capacity than byobRequest");r.buffer=t.buffer,Ee(e,t.byteLength)}(this._associatedReadableByteStreamController,e)}}Object.defineProperties(he.prototype,{respond:{enumerable:!0},respondWithNewView:{enumerable:!0},view:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(he.prototype,o.toStringTag,{value:"ReadableStreamBYOBRequest",configurable:!0});class ye{constructor(){throw new TypeError("Illegal constructor")}get byobRequest(){if(!be(this))throw Be("byobRequest");if(null===this._byobRequest&&this._pendingPullIntos.length>0){const e=this._pendingPullIntos.peek(),t=new Uint8Array(e.buffer,e.byteOffset+e.bytesFilled,e.byteLength-e.bytesFilled),r=Object.create(he.prototype);!function(e,t,r){e._associatedReadableByteStreamController=t,e._view=r}(r,this,t),this._byobRequest=r}return this._byobRequest}get desiredSize(){if(!be(this))throw Be("desiredSize");return Ce(this)}close(){if(!be(this))throw Be("close");if(this._closeRequested)throw new TypeError("The stream has already been closed; do not close it again!");const e=this._controlledReadableByteStream._state;if("readable"!==e)throw new TypeError(`The stream (in ${e} state) is not in the readable state and cannot be closed`);!function(e){const t=e._controlledReadableByteStream;if(!e._closeRequested&&"readable"===t._state)if(e._queueTotalSize>0)e._closeRequested=!0;else{if(e._pendingPullIntos.length>0&&e._pendingPullIntos.peek().bytesFilled>0){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");throw Oe(e,t),t}ke(e),ur(t)}}(this)}enqueue(e){if(!be(this))throw Be("enqueue");if(F(e,1,"enqueue"),!ArrayBuffer.isView(e))throw new TypeError("chunk must be an array buffer view");if(0===e.byteLength)throw new TypeError("chunk must have non-zero byteLength");if(0===e.buffer.byteLength)throw new TypeError("chunk's buffer must have non-zero byteLength");if(this._closeRequested)throw new TypeError("stream is closed or draining");const t=this._controlledReadableByteStream._state;if("readable"!==t)throw new TypeError(`The stream (in ${t} state) is not in the readable state and cannot be enqueued to`);!function(e,t){const r=e._controlledReadableByteStream;if(e._closeRequested||"readable"!==r._state)return;const o=t.buffer,n=t.byteOffset,s=t.byteLength,i=o;Z(r)?0===K(r)?ve(e,i,n,s):J(r,new Uint8Array(i,n,s),!1):Le(r)?(ve(e,i,n,s),Pe(e)):ve(e,i,n,s),_e(e)}(this,e)}error(e){if(!be(this))throw Be("error");Oe(this,e)}[W](e){this._pendingPullIntos.length>0&&(this._pendingPullIntos.peek().bytesFilled=0),fe(this);const t=this._cancelAlgorithm(e);return ke(this),t}[L](e){const t=this._controlledReadableByteStream;if(this._queueTotalSize>0){const t=this._queue.shift();this._queueTotalSize-=t.byteLength,Re(this);const r=new Uint8Array(t.buffer,t.byteOffset,t.byteLength);return void e._chunkSteps(r)}const r=this._autoAllocateChunkSize;if(void 0!==r){let t;try{t=new ArrayBuffer(r)}catch(t){return void e._errorSteps(t)}const o={buffer:t,byteOffset:0,byteLength:r,bytesFilled:0,elementSize:1,viewConstructor:Uint8Array,readerType:"default"};this._pendingPullIntos.push(o)}G(t,e),_e(this)}}function be(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableByteStream")}function me(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_associatedReadableByteStreamController")}function _e(e){(function(e){const t=e._controlledReadableByteStream;return"readable"===t._state&&(!e._closeRequested&&(!!e._started&&(!!(Z(t)&&K(t)>0)||(!!(Le(t)&&We(t)>0)||Ce(e)>0))))})(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,b(e._pullAlgorithm(),(()=>{e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,_e(e))}),(t=>{Oe(e,t)}))))}function ge(e,t){let r=!1;"closed"===e._state&&(r=!0);const o=we(t);"default"===t.readerType?J(e,o,r):function(e,t,r){const o=e._reader._readIntoRequests.shift();r?o._closeSteps(t):o._chunkSteps(t)}(e,o,r)}function we(e){const t=e.bytesFilled,r=e.elementSize;return new e.viewConstructor(e.buffer,e.byteOffset,t/r)}function ve(e,t,r,o){e._queue.push({buffer:t,byteOffset:r,byteLength:o}),e._queueTotalSize+=o}function Se(e,t){const r=t.elementSize,o=t.bytesFilled-t.bytesFilled%r,n=Math.min(e._queueTotalSize,t.byteLength-t.bytesFilled),s=t.bytesFilled+n,i=s-s%r;let a=n,u=!1;i>o&&(a=i-t.bytesFilled,u=!0);const l=e._queue;for(;a>0;){const r=l.peek(),o=Math.min(a,r.byteLength),n=t.byteOffset+t.bytesFilled;c=t.buffer,d=n,f=r.buffer,p=r.byteOffset,h=o,new Uint8Array(c).set(new Uint8Array(f,p,h),d),r.byteLength===o?l.shift():(r.byteOffset+=o,r.byteLength-=o),e._queueTotalSize-=o,Te(e,o,t),a-=o}var c,d,f,p,h;return u}function Te(e,t,r){qe(e),r.bytesFilled+=t}function Re(e){0===e._queueTotalSize&&e._closeRequested?(ke(e),ur(e._controlledReadableByteStream)):_e(e)}function qe(e){null!==e._byobRequest&&(e._byobRequest._associatedReadableByteStreamController=void 0,e._byobRequest._view=null,e._byobRequest=null)}function Pe(e){for(;e._pendingPullIntos.length>0;){if(0===e._queueTotalSize)return;const t=e._pendingPullIntos.peek();Se(e,t)&&(je(e),ge(e._controlledReadableByteStream,t))}}function Ee(e,t){const r=e._pendingPullIntos.peek();if("closed"===e._controlledReadableByteStream._state){if(0!==t)throw new TypeError("bytesWritten must be 0 when calling respond() on a closed stream");!function(e,t){t.buffer=t.buffer;const r=e._controlledReadableByteStream;if(Le(r))for(;We(r)>0;)ge(r,je(e))}(e,r)}else!function(e,t,r){if(r.bytesFilled+t>r.byteLength)throw new RangeError("bytesWritten out of range");if(Te(e,t,r),r.bytesFilled0){const t=r.byteOffset+r.bytesFilled,n=r.buffer.slice(t-o,t);ve(e,n,0,n.byteLength)}r.buffer=r.buffer,r.bytesFilled-=o,ge(e._controlledReadableByteStream,r),Pe(e)}(e,t,r);_e(e)}function je(e){const t=e._pendingPullIntos.shift();return qe(e),t}function ke(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0}function Oe(e,t){const r=e._controlledReadableByteStream;"readable"===r._state&&(function(e){qe(e),e._pendingPullIntos=new R}(e),fe(e),ke(e),lr(r,t))}function Ce(e){const t=e._controlledReadableByteStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function Ae(e){return new TypeError(`ReadableStreamBYOBRequest.prototype.${e} can only be used on a ReadableStreamBYOBRequest`)}function Be(e){return new TypeError(`ReadableByteStreamController.prototype.${e} can only be used on a ReadableByteStreamController`)}function xe(e,t){e._reader._readIntoRequests.push(t)}function We(e){return e._reader._readIntoRequests.length}function Le(e){const t=e._reader;return void 0!==t&&!!Me(t)}Object.defineProperties(ye.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},byobRequest:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ye.prototype,o.toStringTag,{value:"ReadableByteStreamController",configurable:!0});class ze{constructor(e){if(F(e,1,"ReadableStreamBYOBReader"),Q(e,"First parameter"),ir(e))throw new TypeError("This stream has already been locked for exclusive reading by another reader");if(!be(e._readableStreamController))throw new TypeError("Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source");q(this,e),this._readIntoRequests=new R}get closed(){return Me(this)?this._closedPromise:h($e("closed"))}cancel(e){return Me(this)?void 0===this._ownerReadableStream?h(j("cancel")):P(this,e):h($e("cancel"))}read(e){if(!Me(this))return h($e("read"));if(!ArrayBuffer.isView(e))return h(new TypeError("view must be an array buffer view"));if(0===e.byteLength)return h(new TypeError("view must have non-zero byteLength"));if(0===e.buffer.byteLength)return h(new TypeError("view's buffer must have non-zero byteLength"));if(void 0===this._ownerReadableStream)return h(j("read from"));let t,r;const o=f(((e,o)=>{t=e,r=o}));return function(e,t,r){const o=e._ownerReadableStream;o._disturbed=!0,"errored"===o._state?r._errorSteps(o._storedError):function(e,t,r){const o=e._controlledReadableByteStream;let n=1;t.constructor!==DataView&&(n=t.constructor.BYTES_PER_ELEMENT);const s=t.constructor,i={buffer:t.buffer,byteOffset:t.byteOffset,byteLength:t.byteLength,bytesFilled:0,elementSize:n,viewConstructor:s,readerType:"byob"};if(e._pendingPullIntos.length>0)return e._pendingPullIntos.push(i),void xe(o,r);if("closed"!==o._state){if(e._queueTotalSize>0){if(Se(e,i)){const t=we(i);return Re(e),void r._chunkSteps(t)}if(e._closeRequested){const t=new TypeError("Insufficient bytes to fill elements in the given buffer");return Oe(e,t),void r._errorSteps(t)}}e._pendingPullIntos.push(i),xe(o,r),_e(e)}else{const e=new s(i.buffer,i.byteOffset,0);r._closeSteps(e)}}(o._readableStreamController,t,r)}(this,e,{_chunkSteps:e=>t({value:e,done:!1}),_closeSteps:e=>t({value:e,done:!0}),_errorSteps:e=>r(e)}),o}releaseLock(){if(!Me(this))throw $e("releaseLock");if(void 0!==this._ownerReadableStream){if(this._readIntoRequests.length>0)throw new TypeError("Tried to release a reader lock when that reader has pending read() calls un-settled");E(this)}}}function Me(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readIntoRequests")}function $e(e){return new TypeError(`ReadableStreamBYOBReader.prototype.${e} can only be used on a ReadableStreamBYOBReader`)}function Ie(e,t){const{highWaterMark:r}=e;if(void 0===r)return t;if(ue(r)||r<0)throw new RangeError("Invalid highWaterMark");return r}function De(e){const{size:t}=e;return t||(()=>1)}function Fe(e,t){$(e,t);const r=null==e?void 0:e.highWaterMark,o=null==e?void 0:e.size;return{highWaterMark:void 0===r?void 0:H(r),size:void 0===o?void 0:Ue(o,`${t} has member 'size' that`)}}function Ue(e,t){return I(e,t),t=>H(e(t))}function He(e,t,r){return I(e,r),r=>T(e,t,[r])}function Ne(e,t,r){return I(e,r),()=>T(e,t,[])}function Ve(e,t,r){return I(e,r),r=>S(e,t,[r])}function Qe(e,t,r){return I(e,r),(r,o)=>T(e,t,[r,o])}function Ye(e,t){if(!Ze(e))throw new TypeError(`${t} is not a WritableStream.`)}Object.defineProperties(ze.prototype,{cancel:{enumerable:!0},read:{enumerable:!0},releaseLock:{enumerable:!0},closed:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ze.prototype,o.toStringTag,{value:"ReadableStreamBYOBReader",configurable:!0});class Ge{constructor(e={},t={}){void 0===e?e=null:D(e,"First parameter");const r=Fe(t,"Second parameter"),o=function(e,t){$(e,t);const r=null==e?void 0:e.abort,o=null==e?void 0:e.close,n=null==e?void 0:e.start,s=null==e?void 0:e.type,i=null==e?void 0:e.write;return{abort:void 0===r?void 0:He(r,e,`${t} has member 'abort' that`),close:void 0===o?void 0:Ne(o,e,`${t} has member 'close' that`),start:void 0===n?void 0:Ve(n,e,`${t} has member 'start' that`),write:void 0===i?void 0:Qe(i,e,`${t} has member 'write' that`),type:s}}(e,"First parameter");if(Ke(this),void 0!==o.type)throw new RangeError("Invalid type is specified");const n=De(r);!function(e,t,r,o){const n=Object.create(yt.prototype);let s=()=>{},i=()=>p(void 0),a=()=>p(void 0),u=()=>p(void 0);void 0!==t.start&&(s=()=>t.start(n)),void 0!==t.write&&(i=e=>t.write(e,n)),void 0!==t.close&&(a=()=>t.close()),void 0!==t.abort&&(u=e=>t.abort(e)),bt(e,n,s,i,a,u,r,o)}(this,o,Ie(r,1),n)}get locked(){if(!Ze(this))throw Tt("locked");return Xe(this)}abort(e){return Ze(this)?Xe(this)?h(new TypeError("Cannot abort a stream that already has a writer")):et(this,e):h(Tt("abort"))}close(){return Ze(this)?Xe(this)?h(new TypeError("Cannot close a stream that already has a writer")):st(this)?h(new TypeError("Cannot close an already-closing stream")):tt(this):h(Tt("close"))}getWriter(){if(!Ze(this))throw Tt("getWriter");return Je(this)}}function Je(e){return new ut(e)}function Ke(e){e._state="writable",e._storedError=void 0,e._writer=void 0,e._writableStreamController=void 0,e._writeRequests=new R,e._inFlightWriteRequest=void 0,e._closeRequest=void 0,e._inFlightCloseRequest=void 0,e._pendingAbortRequest=void 0,e._backpressure=!1}function Ze(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_writableStreamController")}function Xe(e){return void 0!==e._writer}function et(e,t){const r=e._state;if("closed"===r||"errored"===r)return p(void 0);if(void 0!==e._pendingAbortRequest)return e._pendingAbortRequest._promise;let o=!1;"erroring"===r&&(o=!0,t=void 0);const n=f(((r,n)=>{e._pendingAbortRequest={_promise:void 0,_resolve:r,_reject:n,_reason:t,_wasAlreadyErroring:o}}));return e._pendingAbortRequest._promise=n,o||ot(e,t),n}function tt(e){const t=e._state;if("closed"===t||"errored"===t)return h(new TypeError(`The stream (in ${t} state) is not in the writable state and cannot be closed`));const r=f(((t,r)=>{const o={_resolve:t,_reject:r};e._closeRequest=o})),o=e._writer;var n;return void 0!==o&&e._backpressure&&"writable"===t&&xt(o),de(n=e._writableStreamController,ht,0),gt(n),r}function rt(e,t){"writable"!==e._state?nt(e):ot(e,t)}function ot(e,t){const r=e._writableStreamController;e._state="erroring",e._storedError=t;const o=e._writer;void 0!==o&&dt(o,t),!function(e){return void 0!==e._inFlightWriteRequest||void 0!==e._inFlightCloseRequest}(e)&&r._started&&nt(e)}function nt(e){e._state="errored",e._writableStreamController[x]();const t=e._storedError;if(e._writeRequests.forEach((e=>{e._reject(t)})),e._writeRequests=new R,void 0===e._pendingAbortRequest)return void it(e);const r=e._pendingAbortRequest;if(e._pendingAbortRequest=void 0,r._wasAlreadyErroring)return r._reject(t),void it(e);b(e._writableStreamController[B](r._reason),(()=>{r._resolve(),it(e)}),(t=>{r._reject(t),it(e)}))}function st(e){return void 0!==e._closeRequest||void 0!==e._inFlightCloseRequest}function it(e){void 0!==e._closeRequest&&(e._closeRequest._reject(e._storedError),e._closeRequest=void 0);const t=e._writer;void 0!==t&&jt(t,e._storedError)}function at(e,t){const r=e._writer;void 0!==r&&t!==e._backpressure&&(t?function(e){Ot(e)}(r):xt(r)),e._backpressure=t}Object.defineProperties(Ge.prototype,{abort:{enumerable:!0},close:{enumerable:!0},getWriter:{enumerable:!0},locked:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Ge.prototype,o.toStringTag,{value:"WritableStream",configurable:!0});class ut{constructor(e){if(F(e,1,"WritableStreamDefaultWriter"),Ye(e,"First parameter"),Xe(e))throw new TypeError("This stream has already been locked for exclusive writing by another writer");this._ownerWritableStream=e,e._writer=this;const t=e._state;if("writable"===t)!st(e)&&e._backpressure?Ot(this):At(this),Pt(this);else if("erroring"===t)Ct(this,e._storedError),Pt(this);else if("closed"===t)At(this),Pt(this),kt(this);else{const t=e._storedError;Ct(this,t),Et(this,t)}}get closed(){return lt(this)?this._closedPromise:h(Rt("closed"))}get desiredSize(){if(!lt(this))throw Rt("desiredSize");if(void 0===this._ownerWritableStream)throw qt("desiredSize");return function(e){const t=e._ownerWritableStream,r=t._state;return"errored"===r||"erroring"===r?null:"closed"===r?0:_t(t._writableStreamController)}(this)}get ready(){return lt(this)?this._readyPromise:h(Rt("ready"))}abort(e){return lt(this)?void 0===this._ownerWritableStream?h(qt("abort")):function(e,t){return et(e._ownerWritableStream,t)}(this,e):h(Rt("abort"))}close(){if(!lt(this))return h(Rt("close"));const e=this._ownerWritableStream;return void 0===e?h(qt("close")):st(e)?h(new TypeError("Cannot close an already-closing stream")):ct(this)}releaseLock(){if(!lt(this))throw Rt("releaseLock");void 0!==this._ownerWritableStream&&ft(this)}write(e){return lt(this)?void 0===this._ownerWritableStream?h(qt("write to")):pt(this,e):h(Rt("write"))}}function lt(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_ownerWritableStream")}function ct(e){return tt(e._ownerWritableStream)}function dt(e,t){"pending"===e._readyPromiseState?Bt(e,t):function(e,t){Ct(e,t)}(e,t)}function ft(e){const t=e._ownerWritableStream,r=new TypeError("Writer was released and can no longer be used to monitor the stream's closedness");dt(e,r),function(e,t){"pending"===e._closedPromiseState?jt(e,t):function(e,t){Et(e,t)}(e,t)}(e,r),t._writer=void 0,e._ownerWritableStream=void 0}function pt(e,t){const r=e._ownerWritableStream,o=r._writableStreamController,n=function(e,t){try{return e._strategySizeAlgorithm(t)}catch(t){return wt(e,t),1}}(o,t);if(r!==e._ownerWritableStream)return h(qt("write to"));const s=r._state;if("errored"===s)return h(r._storedError);if(st(r)||"closed"===s)return h(new TypeError("The stream is closing or closed and cannot be written to"));if("erroring"===s)return h(r._storedError);const i=function(e){return f(((t,r)=>{const o={_resolve:t,_reject:r};e._writeRequests.push(o)}))}(r);return function(e,t,r){try{de(e,t,r)}catch(t){return void wt(e,t)}const o=e._controlledWritableStream;st(o)||"writable"!==o._state||at(o,vt(e)),gt(e)}(o,t,n),i}Object.defineProperties(ut.prototype,{abort:{enumerable:!0},close:{enumerable:!0},releaseLock:{enumerable:!0},write:{enumerable:!0},closed:{enumerable:!0},desiredSize:{enumerable:!0},ready:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(ut.prototype,o.toStringTag,{value:"WritableStreamDefaultWriter",configurable:!0});const ht={};class yt{constructor(){throw new TypeError("Illegal constructor")}error(e){if(!i(t=this)||!Object.prototype.hasOwnProperty.call(t,"_controlledWritableStream"))throw new TypeError("WritableStreamDefaultController.prototype.error can only be used on a WritableStreamDefaultController");var t;"writable"===this._controlledWritableStream._state&&St(this,e)}[B](e){const t=this._abortAlgorithm(e);return mt(this),t}[x](){fe(this)}}function bt(e,t,r,o,n,s,i,a){t._controlledWritableStream=e,e._writableStreamController=t,t._queue=void 0,t._queueTotalSize=void 0,fe(t),t._started=!1,t._strategySizeAlgorithm=a,t._strategyHWM=i,t._writeAlgorithm=o,t._closeAlgorithm=n,t._abortAlgorithm=s;const u=vt(t);at(e,u),b(p(r()),(()=>{t._started=!0,gt(t)}),(r=>{t._started=!0,rt(e,r)}))}function mt(e){e._writeAlgorithm=void 0,e._closeAlgorithm=void 0,e._abortAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function _t(e){return e._strategyHWM-e._queueTotalSize}function gt(e){const t=e._controlledWritableStream;if(!e._started)return;if(void 0!==t._inFlightWriteRequest)return;if("erroring"===t._state)return void nt(t);if(0===e._queue.length)return;const r=e._queue.peek().value;r===ht?function(e){const t=e._controlledWritableStream;(function(e){e._inFlightCloseRequest=e._closeRequest,e._closeRequest=void 0})(t),ce(e);const r=e._closeAlgorithm();mt(e),b(r,(()=>{!function(e){e._inFlightCloseRequest._resolve(void 0),e._inFlightCloseRequest=void 0,"erroring"===e._state&&(e._storedError=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._resolve(),e._pendingAbortRequest=void 0)),e._state="closed";const t=e._writer;void 0!==t&&kt(t)}(t)}),(e=>{!function(e,t){e._inFlightCloseRequest._reject(t),e._inFlightCloseRequest=void 0,void 0!==e._pendingAbortRequest&&(e._pendingAbortRequest._reject(t),e._pendingAbortRequest=void 0),rt(e,t)}(t,e)}))}(e):function(e,t){const r=e._controlledWritableStream;!function(e){e._inFlightWriteRequest=e._writeRequests.shift()}(r),b(e._writeAlgorithm(t),(()=>{!function(e){e._inFlightWriteRequest._resolve(void 0),e._inFlightWriteRequest=void 0}(r);const t=r._state;if(ce(e),!st(r)&&"writable"===t){const t=vt(e);at(r,t)}gt(e)}),(t=>{"writable"===r._state&&mt(e),function(e,t){e._inFlightWriteRequest._reject(t),e._inFlightWriteRequest=void 0,rt(e,t)}(r,t)}))}(e,r)}function wt(e,t){"writable"===e._controlledWritableStream._state&&St(e,t)}function vt(e){return _t(e)<=0}function St(e,t){const r=e._controlledWritableStream;mt(e),ot(r,t)}function Tt(e){return new TypeError(`WritableStream.prototype.${e} can only be used on a WritableStream`)}function Rt(e){return new TypeError(`WritableStreamDefaultWriter.prototype.${e} can only be used on a WritableStreamDefaultWriter`)}function qt(e){return new TypeError("Cannot "+e+" a stream using a released writer")}function Pt(e){e._closedPromise=f(((t,r)=>{e._closedPromise_resolve=t,e._closedPromise_reject=r,e._closedPromiseState="pending"}))}function Et(e,t){Pt(e),jt(e,t)}function jt(e,t){void 0!==e._closedPromise_reject&&(w(e._closedPromise),e._closedPromise_reject(t),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="rejected")}function kt(e){void 0!==e._closedPromise_resolve&&(e._closedPromise_resolve(void 0),e._closedPromise_resolve=void 0,e._closedPromise_reject=void 0,e._closedPromiseState="resolved")}function Ot(e){e._readyPromise=f(((t,r)=>{e._readyPromise_resolve=t,e._readyPromise_reject=r})),e._readyPromiseState="pending"}function Ct(e,t){Ot(e),Bt(e,t)}function At(e){Ot(e),xt(e)}function Bt(e,t){void 0!==e._readyPromise_reject&&(w(e._readyPromise),e._readyPromise_reject(t),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="rejected")}function xt(e){void 0!==e._readyPromise_resolve&&(e._readyPromise_resolve(void 0),e._readyPromise_resolve=void 0,e._readyPromise_reject=void 0,e._readyPromiseState="fulfilled")}Object.defineProperties(yt.prototype,{error:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(yt.prototype,o.toStringTag,{value:"WritableStreamDefaultController",configurable:!0});const Wt="undefined"!=typeof DOMException?DOMException:void 0,Lt=function(e){if("function"!=typeof e&&"object"!=typeof e)return!1;try{return new e,!0}catch(e){return!1}}(Wt)?Wt:function(){const e=function(e,t){this.message=e||"",this.name=t||"Error",Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)};return e.prototype=Object.create(Error.prototype),Object.defineProperty(e.prototype,"constructor",{value:e,writable:!0,configurable:!0}),e}();function zt(e,t,r,o,s,i){const a=Y(e),u=Je(t);e._disturbed=!0;let l=!1,c=p(void 0);return f(((d,g)=>{let v;if(void 0!==i){if(v=()=>{const r=new Lt("Aborted","AbortError"),n=[];o||n.push((()=>"writable"===t._state?et(t,r):p(void 0))),s||n.push((()=>"readable"===e._state?ar(e,r):p(void 0))),j((()=>Promise.all(n.map((e=>e())))),!0,r)},i.aborted)return void v();i.addEventListener("abort",v)}var S,T,R;if(P(e,a._closedPromise,(e=>{o?k(!0,e):j((()=>et(t,e)),!0,e)})),P(t,u._closedPromise,(t=>{s?k(!0,t):j((()=>ar(e,t)),!0,t)})),S=e,T=a._closedPromise,R=()=>{r?k():j((()=>function(e){const t=e._ownerWritableStream,r=t._state;return st(t)||"closed"===r?p(void 0):"errored"===r?h(t._storedError):ct(e)}(u)))},"closed"===S._state?R():m(T,R),st(t)||"closed"===t._state){const t=new TypeError("the destination writable stream closed before all data could be piped to it");s?k(!0,t):j((()=>ar(e,t)),!0,t)}function q(){const e=c;return y(c,(()=>e!==c?q():void 0))}function P(e,t,r){"errored"===e._state?r(e._storedError):_(t,r)}function j(e,r,o){function n(){b(e(),(()=>O(r,o)),(e=>O(!0,e)))}l||(l=!0,"writable"!==t._state||st(t)?n():m(q(),n))}function k(e,r){l||(l=!0,"writable"!==t._state||st(t)?O(e,r):m(q(),(()=>O(e,r))))}function O(e,t){ft(u),E(a),void 0!==i&&i.removeEventListener("abort",v),e?g(t):d(void 0)}w(f(((e,t)=>{!function r(o){o?e():y(l?p(!0):y(u._readyPromise,(()=>f(((e,t)=>{te(a,{_chunkSteps:t=>{c=y(pt(u,t),void 0,n),e(!1)},_closeSteps:()=>e(!0),_errorSteps:t})})))),r,t)}(!1)})))}))}class Mt{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!$t(this))throw Gt("desiredSize");return Vt(this)}close(){if(!$t(this))throw Gt("close");if(!Qt(this))throw new TypeError("The stream is not in a state that permits close");Ut(this)}enqueue(e){if(!$t(this))throw Gt("enqueue");if(!Qt(this))throw new TypeError("The stream is not in a state that permits enqueue");return Ht(this,e)}error(e){if(!$t(this))throw Gt("error");Nt(this,e)}[W](e){fe(this);const t=this._cancelAlgorithm(e);return Ft(this),t}[L](e){const t=this._controlledReadableStream;if(this._queue.length>0){const r=ce(this);this._closeRequested&&0===this._queue.length?(Ft(this),ur(t)):It(this),e._chunkSteps(r)}else G(t,e),It(this)}}function $t(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledReadableStream")}function It(e){Dt(e)&&(e._pulling?e._pullAgain=!0:(e._pulling=!0,b(e._pullAlgorithm(),(()=>{e._pulling=!1,e._pullAgain&&(e._pullAgain=!1,It(e))}),(t=>{Nt(e,t)}))))}function Dt(e){const t=e._controlledReadableStream;return!!Qt(e)&&(!!e._started&&(!!(ir(t)&&K(t)>0)||Vt(e)>0))}function Ft(e){e._pullAlgorithm=void 0,e._cancelAlgorithm=void 0,e._strategySizeAlgorithm=void 0}function Ut(e){if(!Qt(e))return;const t=e._controlledReadableStream;e._closeRequested=!0,0===e._queue.length&&(Ft(e),ur(t))}function Ht(e,t){if(!Qt(e))return;const r=e._controlledReadableStream;if(ir(r)&&K(r)>0)J(r,t,!1);else{let r;try{r=e._strategySizeAlgorithm(t)}catch(t){throw Nt(e,t),t}try{de(e,t,r)}catch(t){throw Nt(e,t),t}}It(e)}function Nt(e,t){const r=e._controlledReadableStream;"readable"===r._state&&(fe(e),Ft(e),lr(r,t))}function Vt(e){const t=e._controlledReadableStream._state;return"errored"===t?null:"closed"===t?0:e._strategyHWM-e._queueTotalSize}function Qt(e){const t=e._controlledReadableStream._state;return!e._closeRequested&&"readable"===t}function Yt(e,t,r,o,n,s,i){t._controlledReadableStream=e,t._queue=void 0,t._queueTotalSize=void 0,fe(t),t._started=!1,t._closeRequested=!1,t._pullAgain=!1,t._pulling=!1,t._strategySizeAlgorithm=i,t._strategyHWM=s,t._pullAlgorithm=o,t._cancelAlgorithm=n,e._readableStreamController=t,b(p(r()),(()=>{t._started=!0,It(t)}),(e=>{Nt(t,e)}))}function Gt(e){return new TypeError(`ReadableStreamDefaultController.prototype.${e} can only be used on a ReadableStreamDefaultController`)}function Jt(e,t,r){return I(e,r),r=>T(e,t,[r])}function Kt(e,t,r){return I(e,r),r=>T(e,t,[r])}function Zt(e,t,r){return I(e,r),r=>S(e,t,[r])}function Xt(e,t){if("bytes"!=(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamType`);return e}function er(e,t){if("byob"!=(e=`${e}`))throw new TypeError(`${t} '${e}' is not a valid enumeration value for ReadableStreamReaderMode`);return e}function tr(e,t){$(e,t);const r=null==e?void 0:e.preventAbort,o=null==e?void 0:e.preventCancel,n=null==e?void 0:e.preventClose,s=null==e?void 0:e.signal;return void 0!==s&&function(e,t){if(!function(e){if("object"!=typeof e||null===e)return!1;try{return"boolean"==typeof e.aborted}catch(e){return!1}}(e))throw new TypeError(`${t} is not an AbortSignal.`)}(s,`${t} has member 'signal' that`),{preventAbort:Boolean(r),preventCancel:Boolean(o),preventClose:Boolean(n),signal:s}}Object.defineProperties(Mt.prototype,{close:{enumerable:!0},enqueue:{enumerable:!0},error:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Mt.prototype,o.toStringTag,{value:"ReadableStreamDefaultController",configurable:!0});class rr{constructor(e={},t={}){void 0===e?e=null:D(e,"First parameter");const r=Fe(t,"Second parameter"),o=function(e,t){$(e,t);const r=e,o=null==r?void 0:r.autoAllocateChunkSize,n=null==r?void 0:r.cancel,s=null==r?void 0:r.pull,i=null==r?void 0:r.start,a=null==r?void 0:r.type;return{autoAllocateChunkSize:void 0===o?void 0:V(o,`${t} has member 'autoAllocateChunkSize' that`),cancel:void 0===n?void 0:Jt(n,r,`${t} has member 'cancel' that`),pull:void 0===s?void 0:Kt(s,r,`${t} has member 'pull' that`),start:void 0===i?void 0:Zt(i,r,`${t} has member 'start' that`),type:void 0===a?void 0:Xt(a,`${t} has member 'type' that`)}}(e,"First parameter");if(nr(this),"bytes"===o.type){if(void 0!==r.size)throw new RangeError("The strategy for a byte stream cannot have a size function");!function(e,t,r){const o=Object.create(ye.prototype);let n=()=>{},s=()=>p(void 0),i=()=>p(void 0);void 0!==t.start&&(n=()=>t.start(o)),void 0!==t.pull&&(s=()=>t.pull(o)),void 0!==t.cancel&&(i=e=>t.cancel(e));const a=t.autoAllocateChunkSize;!function(e,t,r,o,n,s,i){t._controlledReadableByteStream=e,t._pullAgain=!1,t._pulling=!1,t._byobRequest=null,t._queue=t._queueTotalSize=void 0,fe(t),t._closeRequested=!1,t._started=!1,t._strategyHWM=s,t._pullAlgorithm=o,t._cancelAlgorithm=n,t._autoAllocateChunkSize=i,t._pendingPullIntos=new R,e._readableStreamController=t,b(p(r()),(()=>{t._started=!0,_e(t)}),(e=>{Oe(t,e)}))}(e,o,n,s,i,r,a)}(this,o,Ie(r,0))}else{const e=De(r);!function(e,t,r,o){const n=Object.create(Mt.prototype);let s=()=>{},i=()=>p(void 0),a=()=>p(void 0);void 0!==t.start&&(s=()=>t.start(n)),void 0!==t.pull&&(i=()=>t.pull(n)),void 0!==t.cancel&&(a=e=>t.cancel(e)),Yt(e,n,s,i,a,r,o)}(this,o,Ie(r,1),e)}}get locked(){if(!sr(this))throw cr("locked");return ir(this)}cancel(e){return sr(this)?ir(this)?h(new TypeError("Cannot cancel a stream that already has a reader")):ar(this,e):h(cr("cancel"))}getReader(e){if(!sr(this))throw cr("getReader");return void 0===function(e,t){$(e,t);const r=null==e?void 0:e.mode;return{mode:void 0===r?void 0:er(r,`${t} has member 'mode' that`)}}(e,"First parameter").mode?Y(this):new ze(this)}pipeThrough(e,t={}){if(!sr(this))throw cr("pipeThrough");F(e,1,"pipeThrough");const r=function(e,t){$(e,t);const r=null==e?void 0:e.readable;U(r,"readable","ReadableWritablePair"),Q(r,`${t} has member 'readable' that`);const o=null==e?void 0:e.writable;return U(o,"writable","ReadableWritablePair"),Ye(o,`${t} has member 'writable' that`),{readable:r,writable:o}}(e,"First parameter"),o=tr(t,"Second parameter");if(ir(this))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream");if(Xe(r.writable))throw new TypeError("ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream");return w(zt(this,r.writable,o.preventClose,o.preventAbort,o.preventCancel,o.signal)),r.readable}pipeTo(e,t={}){if(!sr(this))return h(cr("pipeTo"));if(void 0===e)return h("Parameter 1 is required in 'pipeTo'.");if(!Ze(e))return h(new TypeError("ReadableStream.prototype.pipeTo's first argument must be a WritableStream"));let r;try{r=tr(t,"Second parameter")}catch(e){return h(e)}return ir(this)?h(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream")):Xe(e)?h(new TypeError("ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream")):zt(this,e,r.preventClose,r.preventAbort,r.preventCancel,r.signal)}tee(){if(!sr(this))throw cr("tee");const e=function(e,t){const r=Y(e);let o,n,s,i,a,u=!1,l=!1,c=!1;const d=f((e=>{a=e}));function h(){return u||(u=!0,te(r,{_chunkSteps:e=>{v((()=>{u=!1;const t=e,r=e;l||Ht(s._readableStreamController,t),c||Ht(i._readableStreamController,r),a(void 0)}))},_closeSteps:()=>{u=!1,l||Ut(s._readableStreamController),c||Ut(i._readableStreamController)},_errorSteps:()=>{u=!1}})),p(void 0)}function y(){}return s=or(y,h,(function(t){if(l=!0,o=t,c){const t=pe([o,n]),r=ar(e,t);a(r)}return d})),i=or(y,h,(function(t){if(c=!0,n=t,l){const t=pe([o,n]),r=ar(e,t);a(r)}return d})),_(r._closedPromise,(e=>{Nt(s._readableStreamController,e),Nt(i._readableStreamController,e),a(void 0)})),[s,i]}(this);return pe(e)}values(e){if(!sr(this))throw cr("values");return function(e,t){const r=Y(e),o=new ne(r,t),n=Object.create(se);return n._asyncIteratorImpl=o,n}(this,function(e,t){$(e,"First parameter");const r=null==e?void 0:e.preventCancel;return{preventCancel:Boolean(r)}}(e).preventCancel)}}function or(e,t,r,o=1,n=(()=>1)){const s=Object.create(rr.prototype);return nr(s),Yt(s,Object.create(Mt.prototype),e,t,r,o,n),s}function nr(e){e._state="readable",e._reader=void 0,e._storedError=void 0,e._disturbed=!1}function sr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_readableStreamController")}function ir(e){return void 0!==e._reader}function ar(e,t){return e._disturbed=!0,"closed"===e._state?p(void 0):"errored"===e._state?h(e._storedError):(ur(e),g(e._readableStreamController[W](t),n))}function ur(e){e._state="closed";const t=e._reader;void 0!==t&&(ee(t)&&(t._readRequests.forEach((e=>{e._closeSteps()})),t._readRequests=new R),A(t))}function lr(e,t){e._state="errored",e._storedError=t;const r=e._reader;void 0!==r&&(ee(r)?(r._readRequests.forEach((e=>{e._errorSteps(t)})),r._readRequests=new R):(r._readIntoRequests.forEach((e=>{e._errorSteps(t)})),r._readIntoRequests=new R),C(r,t))}function cr(e){return new TypeError(`ReadableStream.prototype.${e} can only be used on a ReadableStream`)}function dr(e,t){$(e,t);const r=null==e?void 0:e.highWaterMark;return U(r,"highWaterMark","QueuingStrategyInit"),{highWaterMark:H(r)}}Object.defineProperties(rr.prototype,{cancel:{enumerable:!0},getReader:{enumerable:!0},pipeThrough:{enumerable:!0},pipeTo:{enumerable:!0},tee:{enumerable:!0},values:{enumerable:!0},locked:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(rr.prototype,o.toStringTag,{value:"ReadableStream",configurable:!0}),"symbol"==typeof o.asyncIterator&&Object.defineProperty(rr.prototype,o.asyncIterator,{value:rr.prototype.values,writable:!0,configurable:!0});const fr=function(e){return e.byteLength};class pr{constructor(e){F(e,1,"ByteLengthQueuingStrategy"),e=dr(e,"First parameter"),this._byteLengthQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!yr(this))throw hr("highWaterMark");return this._byteLengthQueuingStrategyHighWaterMark}get size(){if(!yr(this))throw hr("size");return fr}}function hr(e){return new TypeError(`ByteLengthQueuingStrategy.prototype.${e} can only be used on a ByteLengthQueuingStrategy`)}function yr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_byteLengthQueuingStrategyHighWaterMark")}Object.defineProperties(pr.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(pr.prototype,o.toStringTag,{value:"ByteLengthQueuingStrategy",configurable:!0});const br=function(){return 1};class mr{constructor(e){F(e,1,"CountQueuingStrategy"),e=dr(e,"First parameter"),this._countQueuingStrategyHighWaterMark=e.highWaterMark}get highWaterMark(){if(!gr(this))throw _r("highWaterMark");return this._countQueuingStrategyHighWaterMark}get size(){if(!gr(this))throw _r("size");return br}}function _r(e){return new TypeError(`CountQueuingStrategy.prototype.${e} can only be used on a CountQueuingStrategy`)}function gr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_countQueuingStrategyHighWaterMark")}function wr(e,t,r){return I(e,r),r=>T(e,t,[r])}function vr(e,t,r){return I(e,r),r=>S(e,t,[r])}function Sr(e,t,r){return I(e,r),(r,o)=>T(e,t,[r,o])}Object.defineProperties(mr.prototype,{highWaterMark:{enumerable:!0},size:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(mr.prototype,o.toStringTag,{value:"CountQueuingStrategy",configurable:!0});class Tr{constructor(e={},t={},r={}){void 0===e&&(e=null);const o=Fe(t,"Second parameter"),n=Fe(r,"Third parameter"),s=function(e,t){$(e,t);const r=null==e?void 0:e.flush,o=null==e?void 0:e.readableType,n=null==e?void 0:e.start,s=null==e?void 0:e.transform,i=null==e?void 0:e.writableType;return{flush:void 0===r?void 0:wr(r,e,`${t} has member 'flush' that`),readableType:o,start:void 0===n?void 0:vr(n,e,`${t} has member 'start' that`),transform:void 0===s?void 0:Sr(s,e,`${t} has member 'transform' that`),writableType:i}}(e,"First parameter");if(void 0!==s.readableType)throw new RangeError("Invalid readableType specified");if(void 0!==s.writableType)throw new RangeError("Invalid writableType specified");const i=Ie(n,0),a=De(n),u=Ie(o,1),l=De(o);let c;!function(e,t,r,o,n,s){function i(){return t}e._writable=function(e,t,r,o,n=1,s=(()=>1)){const i=Object.create(Ge.prototype);return Ke(i),bt(i,Object.create(yt.prototype),e,t,r,o,n,s),i}(i,(function(t){return function(e,t){const r=e._transformStreamController;return e._backpressure?g(e._backpressureChangePromise,(()=>{const o=e._writable;if("erroring"===o._state)throw o._storedError;return Ar(r,t)})):Ar(r,t)}(e,t)}),(function(){return function(e){const t=e._readable,r=e._transformStreamController,o=r._flushAlgorithm();return Or(r),g(o,(()=>{if("errored"===t._state)throw t._storedError;Ut(t._readableStreamController)}),(r=>{throw qr(e,r),t._storedError}))}(e)}),(function(t){return function(e,t){return qr(e,t),p(void 0)}(e,t)}),r,o),e._readable=or(i,(function(){return function(e){return Er(e,!1),e._backpressureChangePromise}(e)}),(function(t){return Pr(e,t),p(void 0)}),n,s),e._backpressure=void 0,e._backpressureChangePromise=void 0,e._backpressureChangePromise_resolve=void 0,Er(e,!0),e._transformStreamController=void 0}(this,f((e=>{c=e})),u,l,i,a),function(e,t){const r=Object.create(jr.prototype);let o=e=>{try{return Cr(r,e),p(void 0)}catch(e){return h(e)}},n=()=>p(void 0);void 0!==t.transform&&(o=e=>t.transform(e,r)),void 0!==t.flush&&(n=()=>t.flush(r)),function(e,t,r,o){t._controlledTransformStream=e,e._transformStreamController=t,t._transformAlgorithm=r,t._flushAlgorithm=o}(e,r,o,n)}(this,s),void 0!==s.start?c(s.start(this._transformStreamController)):c(void 0)}get readable(){if(!Rr(this))throw xr("readable");return this._readable}get writable(){if(!Rr(this))throw xr("writable");return this._writable}}function Rr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_transformStreamController")}function qr(e,t){Nt(e._readable._readableStreamController,t),Pr(e,t)}function Pr(e,t){Or(e._transformStreamController),wt(e._writable._writableStreamController,t),e._backpressure&&Er(e,!1)}function Er(e,t){void 0!==e._backpressureChangePromise&&e._backpressureChangePromise_resolve(),e._backpressureChangePromise=f((t=>{e._backpressureChangePromise_resolve=t})),e._backpressure=t}Object.defineProperties(Tr.prototype,{readable:{enumerable:!0},writable:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(Tr.prototype,o.toStringTag,{value:"TransformStream",configurable:!0});class jr{constructor(){throw new TypeError("Illegal constructor")}get desiredSize(){if(!kr(this))throw Br("desiredSize");return Vt(this._controlledTransformStream._readable._readableStreamController)}enqueue(e){if(!kr(this))throw Br("enqueue");Cr(this,e)}error(e){if(!kr(this))throw Br("error");var t;t=e,qr(this._controlledTransformStream,t)}terminate(){if(!kr(this))throw Br("terminate");!function(e){const t=e._controlledTransformStream;Ut(t._readable._readableStreamController);Pr(t,new TypeError("TransformStream terminated"))}(this)}}function kr(e){return!!i(e)&&!!Object.prototype.hasOwnProperty.call(e,"_controlledTransformStream")}function Or(e){e._transformAlgorithm=void 0,e._flushAlgorithm=void 0}function Cr(e,t){const r=e._controlledTransformStream,o=r._readable._readableStreamController;if(!Qt(o))throw new TypeError("Readable side is not in a state that permits enqueue");try{Ht(o,t)}catch(e){throw Pr(r,e),r._readable._storedError}(function(e){return!Dt(e)})(o)!==r._backpressure&&Er(r,!0)}function Ar(e,t){return g(e._transformAlgorithm(t),void 0,(t=>{throw qr(e._controlledTransformStream,t),t}))}function Br(e){return new TypeError(`TransformStreamDefaultController.prototype.${e} can only be used on a TransformStreamDefaultController`)}function xr(e){return new TypeError(`TransformStream.prototype.${e} can only be used on a TransformStream`)}Object.defineProperties(jr.prototype,{enqueue:{enumerable:!0},error:{enumerable:!0},terminate:{enumerable:!0},desiredSize:{enumerable:!0}}),"symbol"==typeof o.toStringTag&&Object.defineProperty(jr.prototype,o.toStringTag,{value:"TransformStreamDefaultController",configurable:!0})},417:e=>{"use strict";e.exports=__webpack_require__(417)},605:e=>{"use strict";e.exports=__webpack_require__(605)},211:e=>{"use strict";e.exports=__webpack_require__(211)},413:e=>{"use strict";e.exports=__webpack_require__(413)},835:e=>{"use strict";e.exports=__webpack_require__(835)},669:e=>{"use strict";e.exports=__webpack_require__(669)},761:e=>{"use strict";e.exports=__webpack_require__(761)}},t={};function r(o){if(t[o])return t[o].exports;var n=t[o]={exports:{}};return e[o].call(n.exports,n,n.exports,r),n.exports}return r.d=(e,t)=>{for(var o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r(990)})().default})); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9tYWlsZ3VuL3dlYnBhY2svdW5pdmVyc2FsTW9kdWxlRGVmaW5pdGlvbiIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2Fib3J0LWNvbnRyb2xsZXIvZGlzdC9hYm9ydC1jb250cm9sbGVyLmpzIiwid2VicGFjazovL21haWxndW4vLi9pbmRleC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2NsaWVudC50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2RvbWFpbnMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9lcnJvci50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2V2ZW50cy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL2lwLXBvb2xzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvaXBzLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvbWVzc2FnZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9wYXJzZS50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3JlcXVlc3QudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9yb3V0ZXMudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL2xpYi9zdGF0cy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3N1cHByZXNzaW9ucy50cyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbGliL3ZhbGlkYXRlLnRzIiwid2VicGFjazovL21haWxndW4vLi9saWIvd2ViaG9va3MudHMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9idG9hL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZGF0YS11cmktdG8tYnVmZmVyL2Rpc3Qvc3JjL2luZGV4LmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvZXZlbnQtdGFyZ2V0LXNoaW0vZGlzdC9ldmVudC10YXJnZXQtc2hpbS5qcyIsIndlYnBhY2s6Ly9tYWlsZ3VuLy4vbm9kZV9tb2R1bGVzL2ZldGNoLWJsb2IvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9reS11bml2ZXJzYWwvaW5kZXguanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9reS91bWQuanMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi8uL25vZGVfbW9kdWxlcy9ub2RlLWZldGNoL2Rpc3QvaW5kZXguY2pzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvdXJsLWpvaW4vbGliL3VybC1qb2luLmpzIiwid2VicGFjazovL21haWxndW4vLi9ub2RlX21vZHVsZXMvd2ViLXN0cmVhbXMtcG9seWZpbGwvZGlzdC9wb255ZmlsbC5lczIwMTgubWpzIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJjcnlwdG9cIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwiaHR0cFwiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJodHRwc1wiIiwid2VicGFjazovL21haWxndW4vZXh0ZXJuYWwgXCJzdHJlYW1cIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwidXJsXCIiLCJ3ZWJwYWNrOi8vbWFpbGd1bi9leHRlcm5hbCBcInV0aWxcIiIsIndlYnBhY2s6Ly9tYWlsZ3VuL2V4dGVybmFsIFwiemxpYlwiIiwid2VicGFjazovL21haWxndW4vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3N0YXJ0dXAiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3J1bnRpbWUvZGVmaW5lIHByb3BlcnR5IGdldHRlcnMiLCJ3ZWJwYWNrOi8vbWFpbGd1bi93ZWJwYWNrL3J1bnRpbWUvaGFzT3duUHJvcGVydHkgc2hvcnRoYW5kIiwid2VicGFjazovL21haWxndW4vd2VicGFjay9ydW50aW1lL21ha2UgbmFtZXNwYWNlIG9iamVjdCJdLCJuYW1lcyI6WyJyb290IiwiZmFjdG9yeSIsImV4cG9ydHMiLCJtb2R1bGUiLCJkZWZpbmUiLCJhbWQiLCJ0aGlzIiwiT2JqZWN0IiwiZGVmaW5lUHJvcGVydHkiLCJ2YWx1ZSIsImV2ZW50VGFyZ2V0U2hpbSIsIkFib3J0U2lnbmFsIiwiRXZlbnRUYXJnZXQiLCJzdXBlciIsIlR5cGVFcnJvciIsImFib3J0ZWQiLCJhYm9ydGVkRmxhZ3MiLCJnZXQiLCJkZWZpbmVFdmVudEF0dHJpYnV0ZSIsInByb3RvdHlwZSIsIldlYWtNYXAiLCJkZWZpbmVQcm9wZXJ0aWVzIiwiZW51bWVyYWJsZSIsIlN5bWJvbCIsInRvU3RyaW5nVGFnIiwiY29uZmlndXJhYmxlIiwiQWJvcnRDb250cm9sbGVyIiwic2lnbmFscyIsInNldCIsInNpZ25hbCIsImNyZWF0ZSIsImNhbGwiLCJjcmVhdGVBYm9ydFNpZ25hbCIsImdldFNpZ25hbCIsImRpc3BhdGNoRXZlbnQiLCJ0eXBlIiwiY29udHJvbGxlciIsImFib3J0IiwiZGVmYXVsdCIsIkZvcm1EYXRhIiwiZm9ybURhdGEiLCJjbGllbnQiLCJvcHRpb25zIiwiY29uZmlnIiwidXJsIiwidXNlcm5hbWUiLCJFcnJvciIsImtleSIsInJlcXVlc3QiLCJkb21haW5zIiwid2ViaG9va3MiLCJldmVudHMiLCJzdGF0cyIsInN1cHByZXNzaW9ucyIsIm1lc3NhZ2VzIiwicm91dGVzIiwiaXBzIiwiaXBfcG9vbHMiLCJwdWJsaWNfa2V5IiwicHVibGljX3JlcXVlc3QiLCJ2YWxpZGF0ZSIsInBhcnNlIiwiZGF0YSIsInJlY2VpdmluZyIsInNlbmRpbmciLCJuYW1lIiwicmVxdWlyZV90bHMiLCJza2lwX3ZlcmlmaWNhdGlvbiIsInN0YXRlIiwid2lsZGNhcmQiLCJzcGFtX2FjdGlvbiIsImNyZWF0ZWRfYXQiLCJzbXRwX3Bhc3N3b3JkIiwic210cF9sb2dpbiIsInJlY2VpdmluZ19kbnNfcmVjb3JkcyIsInNlbmRpbmdfZG5zX3JlY29yZHMiLCJfcGFyc2VNZXNzYWdlIiwicmVzcG9uc2UiLCJib2R5IiwiX3BhcnNlRG9tYWluTGlzdCIsIml0ZW1zIiwibWFwIiwiaXRlbSIsIkRvbWFpbiIsIl9wYXJzZURvbWFpbiIsImRvbWFpbiIsIl9wYXJzZVRyYWNraW5nU2V0dGluZ3MiLCJ0cmFja2luZyIsIl9wYXJzZVRyYWNraW5nVXBkYXRlIiwibGlzdCIsInF1ZXJ5IiwidGhlbiIsInBvc3QiLCJkZXN0cm95IiwiZGVsZXRlIiwiZ2V0VHJhY2tpbmciLCJ1cGRhdGVUcmFja2luZyIsInB1dCIsImdldElwcyIsImFzc2lnbklwIiwiaXAiLCJkZWxldGVJcCIsImxpbmtJcFBvb2wiLCJwb29sX2lkIiwidW5saW5rSXBQb2xsIiwic3RhdHVzIiwic3RhdHVzVGV4dCIsIm1lc3NhZ2UiLCJib2R5TWVzc2FnZSIsImVycm9yIiwic3RhY2siLCJkZXRhaWxzIiwidXJsam9pbiIsIl9wYXJzZVBhZ2VOdW1iZXIiLCJzcGxpdCIsInBvcCIsIl9wYXJzZVBhZ2UiLCJpZCIsIm51bWJlciIsIl9wYXJzZVBhZ2VMaW5rcyIsImVudHJpZXMiLCJwYWdpbmciLCJyZWR1Y2UiLCJhY2MiLCJfcGFyc2VFdmVudExpc3QiLCJwYWdlcyIsInBhZ2UiLCJwYXJzZUlwUG9vbHNSZXNwb25zZSIsInVwZGF0ZSIsInBvb2xJZCIsInBhdGNoIiwicGFyc2VJcHNSZXNwb25zZSIsIl9wYXJzZVJlc3BvbnNlIiwicG9zdE11bHRpIiwiYWRkcmVzc2VzIiwiZW5hYmxlRG5zRXNwQ2hlY2tzIiwiQXJyYXkiLCJpc0FycmF5Iiwiam9pbiIsInN5bnRheF9vbmx5IiwiaXNTdHJlYW0iLCJhdHRhY2htZW50IiwicGlwZSIsImdldEF0dGFjaG1lbnRPcHRpb25zIiwiZmlsZW5hbWUiLCJjb250ZW50VHlwZSIsImtub3duTGVuZ3RoIiwiaGVhZGVycyIsIm1ldGhvZCIsImJhc2ljIiwiQXV0aG9yaXphdGlvbiIsInBhcmFtcyIsImdldE93blByb3BlcnR5TmFtZXMiLCJsZW5ndGgiLCJzZWFyY2hQYXJhbXMiLCJ0b0xvY2FsZVVwcGVyQ2FzZSIsInRocm93SHR0cEVycm9ycyIsIm9rIiwic3RyZWFtIiwiY2h1bmtzIiwiUHJvbWlzZSIsInJlc29sdmUiLCJyZWplY3QiLCJvbiIsImNodW5rIiwicHVzaCIsIkJ1ZmZlciIsImNvbmNhdCIsInRvU3RyaW5nIiwianNvbiIsImNvbW1hbmQiLCJoZWFkIiwia2V5cyIsImZpbHRlciIsImZvckVhY2giLCJhcHBlbmQiLCJvYmoiLCJSZXF1ZXN0Iiwic3RhcnQiLCJEYXRlIiwiZW5kIiwicmVzb2x1dGlvbiIsInN0YXQiLCJ0aW1lIiwiX3BhcnNlU3RhdHMiLCJTdGF0cyIsImdldERvbWFpbiIsImdldEFjY291bnQiLCJjcmVhdGVPcHRpb25zIiwiYWRkcmVzcyIsImNvZGUiLCJ0YWdzIiwibW9kZWxzIiwiYm91bmNlcyIsIkJvdW5jZSIsImNvbXBsYWludHMiLCJDb21wbGFpbnQiLCJ1bnN1YnNjcmliZXMiLCJVbnN1YnNjcmliZSIsInBhZ2VVcmwiLCJfcGFyc2VMaXN0IiwiTW9kZWwiLCJkIiwiX3BhcnNlSXRlbSIsIm1vZGVsIiwiZW5jb2RlVVJJQ29tcG9uZW50IiwiU3VwcHJlc3Npb25DbGllbnQiLCJfcGFyc2VXZWJob29rTGlzdCIsIl9wYXJzZVdlYmhvb2tXaXRoSUQiLCJXZWJob29rIiwid2ViaG9vayIsIl9wYXJzZVdlYmhvb2tUZXN0IiwidGVzdCIsInN0ciIsImZyb20iLCJ1cmkiLCJmaXJzdENvbW1hIiwicmVwbGFjZSIsImluZGV4T2YiLCJtZXRhIiwic3Vic3RyaW5nIiwiY2hhcnNldCIsImJhc2U2NCIsInR5cGVGdWxsIiwiaSIsImVuY29kaW5nIiwidW5lc2NhcGUiLCJidWZmZXIiLCJwcml2YXRlRGF0YSIsIndyYXBwZXJzIiwicGQiLCJldmVudCIsInJldHYiLCJjb25zb2xlIiwiYXNzZXJ0Iiwic2V0Q2FuY2VsRmxhZyIsInBhc3NpdmVMaXN0ZW5lciIsImNhbmNlbGFibGUiLCJjYW5jZWxlZCIsInByZXZlbnREZWZhdWx0IiwiRXZlbnQiLCJldmVudFRhcmdldCIsImV2ZW50UGhhc2UiLCJjdXJyZW50VGFyZ2V0Iiwic3RvcHBlZCIsImltbWVkaWF0ZVN0b3BwZWQiLCJ0aW1lU3RhbXAiLCJub3ciLCJkZWZpbmVSZWRpcmVjdERlc2NyaXB0b3IiLCJkZWZpbmVDYWxsRGVzY3JpcHRvciIsImFwcGx5IiwiYXJndW1lbnRzIiwiZ2V0V3JhcHBlciIsInByb3RvIiwid3JhcHBlciIsIkJhc2VFdmVudCIsIkN1c3RvbUV2ZW50IiwiY29uc3RydWN0b3IiLCJ3cml0YWJsZSIsImlzRnVuYyIsImdldE93blByb3BlcnR5RGVzY3JpcHRvciIsImRlZmluZVdyYXBwZXIiLCJnZXRQcm90b3R5cGVPZiIsImlzU3RvcHBlZCIsInNldFBhc3NpdmVMaXN0ZW5lciIsInN0b3BQcm9wYWdhdGlvbiIsInN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbiIsIkJvb2xlYW4iLCJidWJibGVzIiwiY29tcG9zZWQiLCJjYW5jZWxCdWJibGUiLCJ3aW5kb3ciLCJzZXRQcm90b3R5cGVPZiIsImxpc3RlbmVyc01hcCIsImlzT2JqZWN0IiwieCIsImdldExpc3RlbmVycyIsImxpc3RlbmVycyIsImV2ZW50VGFyZ2V0UHJvdG90eXBlIiwiZXZlbnROYW1lIiwibm9kZSIsImxpc3RlbmVyVHlwZSIsImxpc3RlbmVyIiwibmV4dCIsInByZXYiLCJuZXdOb2RlIiwicGFzc2l2ZSIsIm9uY2UiLCJkZWZpbmVFdmVudEF0dHJpYnV0ZURlc2NyaXB0b3IiLCJkZWZpbmVDdXN0b21FdmVudFRhcmdldCIsImV2ZW50TmFtZXMiLCJDdXN0b21FdmVudFRhcmdldCIsInR5cGVzIiwiTWFwIiwib3B0aW9uc0lzT2JqIiwiY2FwdHVyZSIsInVuZGVmaW5lZCIsIndyYXBwZWRFdmVudCIsIndyYXBFdmVudCIsImVyciIsImhhbmRsZUV2ZW50Iiwic2V0RXZlbnRQaGFzZSIsInNldEN1cnJlbnRUYXJnZXQiLCJkZWZhdWx0UHJldmVudGVkIiwiUmVhZGFibGUiLCJ3bSIsIkJsb2IiLCJibG9iUGFydHMiLCJzaXplIiwicGFydHMiLCJlbGVtZW50IiwiQXJyYXlCdWZmZXIiLCJpc1ZpZXciLCJieXRlT2Zmc2V0IiwiYnl0ZUxlbmd0aCIsIlN0cmluZyIsInRvTG93ZXJDYXNlIiwiYXJyYXlCdWZmZXIiLCJVaW50OEFycmF5Iiwib2Zmc2V0IiwiYXN5bmMiLCJwYXJ0IiwicmVhZCIsInJlbGF0aXZlU3RhcnQiLCJNYXRoIiwibWF4IiwibWluIiwicmVsYXRpdmVFbmQiLCJzcGFuIiwidmFsdWVzIiwiYWRkZWQiLCJzbGljZSIsImJsb2IiLCJhc3NpZ24iLCJoYXNJbnN0YW5jZSIsIm9iamVjdCIsImZldGNoIiwiZ2xvYmFsIiwiaGlnaFdhdGVyTWFyayIsIkhlYWRlcnMiLCJSZXNwb25zZSIsIlJlYWRhYmxlU3RyZWFtIiwiXyIsImdsb2JhbHMiLCJnZXRHbG9iYWwiLCJwcm9wZXJ0eSIsInNlbGYiLCJnbG9iYWxUaGlzIiwiZ2xvYmFsUHJvcGVydGllcyIsImdsb2JhbE9iamVjdCIsImJpbmQiLCJzdXBwb3J0c0Fib3J0Q29udHJvbGxlciIsInN1cHBvcnRzU3RyZWFtcyIsInN1cHBvcnRzRm9ybURhdGEiLCJtZXJnZUhlYWRlcnMiLCJzb3VyY2UxIiwic291cmNlMiIsInJlc3VsdCIsImlzSGVhZGVyc0luc3RhbmNlIiwic291cmNlIiwiZGVlcE1lcmdlIiwic291cmNlcyIsInJldHVyblZhbHVlIiwicmVxdWVzdE1ldGhvZHMiLCJyZXNwb25zZVR5cGVzIiwidGV4dCIsInJldHJ5QWZ0ZXJTdGF0dXNDb2RlcyIsInN0b3AiLCJIVFRQRXJyb3IiLCJUaW1lb3V0RXJyb3IiLCJkZWxheSIsIm1zIiwic2V0VGltZW91dCIsIm5vcm1hbGl6ZVJlcXVlc3RNZXRob2QiLCJpbnB1dCIsImluY2x1ZGVzIiwidG9VcHBlckNhc2UiLCJkZWZhdWx0UmV0cnlPcHRpb25zIiwibGltaXQiLCJtZXRob2RzIiwic3RhdHVzQ29kZXMiLCJhZnRlclN0YXR1c0NvZGVzIiwibm9ybWFsaXplUmV0cnlPcHRpb25zIiwicmV0cnkiLCJtYXhTYWZlVGltZW91dCIsIkt5IiwiX3JldHJ5Q291bnQiLCJfaW5wdXQiLCJfb3B0aW9ucyIsImNyZWRlbnRpYWxzIiwiaG9va3MiLCJiZWZvcmVSZXF1ZXN0IiwiYmVmb3JlUmV0cnkiLCJhZnRlclJlc3BvbnNlIiwicHJlZml4VXJsIiwidGltZW91dCIsIlVSTCIsInN0YXJ0c1dpdGgiLCJlbmRzV2l0aCIsImFib3J0Q29udHJvbGxlciIsImFkZEV2ZW50TGlzdGVuZXIiLCJVUkxTZWFyY2hQYXJhbXMiLCJKU09OIiwic3RyaW5naWZ5IiwiZm4iLCJSYW5nZUVycm9yIiwiX2ZldGNoIiwiaG9vayIsIm1vZGlmaWVkUmVzcG9uc2UiLCJfZGVjb3JhdGVSZXNwb25zZSIsImNsb25lIiwib25Eb3dubG9hZFByb2dyZXNzIiwiX3N0cmVhbSIsIl9yZXRyeSIsIm1pbWVUeXBlIiwicGFyc2VKc29uIiwicmV0cnlBZnRlciIsImFmdGVyIiwiTnVtYmVyIiwiaXNOYU4iLCJtYXhSZXRyeUFmdGVyIiwiX2NhbGN1bGF0ZVJldHJ5RGVsYXkiLCJyZXRyeUNvdW50IiwidGltZW91dElEIiwiY2F0Y2giLCJjbGVhclRpbWVvdXQiLCJ0b3RhbEJ5dGVzIiwidHJhbnNmZXJyZWRCeXRlcyIsInJlYWRlciIsImdldFJlYWRlciIsInBlcmNlbnQiLCJkb25lIiwiY2xvc2UiLCJlbnF1ZXVlIiwidmFsaWRhdGVBbmRNZXJnZSIsImNyZWF0ZUluc3RhbmNlIiwiZGVmYXVsdHMiLCJreSIsIm5ld0RlZmF1bHRzIiwiZXh0ZW5kIiwiaHR0cCIsImh0dHBzIiwiemxpYiIsIlN0cmVhbSIsImRhdGFVcmlUb0J1ZmZlciIsInV0aWwiLCJjcnlwdG8iLCJGZXRjaEJhc2VFcnJvciIsImNhcHR1cmVTdGFja1RyYWNlIiwiRmV0Y2hFcnJvciIsInN5c3RlbUVycm9yIiwiZXJybm8iLCJlcnJvcmVkU3lzQ2FsbCIsInN5c2NhbGwiLCJOQU1FIiwiaXNVUkxTZWFyY2hQYXJhbWV0ZXJzIiwiZ2V0QWxsIiwiaGFzIiwic29ydCIsImlzQmxvYiIsImlzRm9ybURhdGEiLCJjYXJyaWFnZSIsImRhc2hlcyIsInJlcGVhdCIsImNhcnJpYWdlTGVuZ3RoIiwiZ2V0Rm9vdGVyIiwiYm91bmRhcnkiLCJnZXRIZWFkZXIiLCJmaWVsZCIsImhlYWRlciIsIklOVEVSTkFMUyIsIkJvZHkiLCJpc0J1ZmZlciIsImlzQW55QXJyYXlCdWZmZXIiLCJyYW5kb21CeXRlcyIsImZvcm0iLCJmb3JtRGF0YUl0ZXJhdG9yIiwiZGlzdHVyYmVkIiwiY29uc3VtZUJvZHkiLCJjdCIsImJ1ZiIsImFsbG9jIiwiYWNjdW0iLCJhY2N1bUJ5dGVzIiwicmVhZGFibGVFbmRlZCIsIl9yZWFkYWJsZVN0YXRlIiwiZW5kZWQiLCJldmVyeSIsImMiLCJib2R5VXNlZCIsImluc3RhbmNlIiwicDEiLCJwMiIsImdldEJvdW5kYXJ5IiwiUGFzc1Rocm91Z2giLCJleHRyYWN0Q29udGVudFR5cGUiLCJ2YWxpZGF0ZUhlYWRlck5hbWUiLCJ2YWxpZGF0ZUhlYWRlclZhbHVlIiwiaW5pdCIsInJhdyIsImlzQm94ZWRQcmltaXRpdmUiLCJpdGVyYXRvciIsInBhaXIiLCJQcm94eSIsInRhcmdldCIsInAiLCJyZWNlaXZlciIsIlNldCIsIlJlZmxlY3QiLCJjYWxsYmFjayIsImZvciIsInJlZGlyZWN0U3RhdHVzIiwiaXNSZWRpcmVjdCIsIklOVEVSTkFMUyQxIiwiY291bnRlciIsInJlZGlyZWN0ZWQiLCJsb2NhdGlvbiIsIklOVEVSTkFMUyQyIiwiaXNSZXF1ZXN0IiwicGFyc2VkVVJMIiwiaW5wdXRCb2R5IiwicmVkaXJlY3QiLCJmb2xsb3ciLCJjb21wcmVzcyIsImFnZW50IiwiaW5zZWN1cmVIVFRQUGFyc2VyIiwiZm9ybWF0IiwiQWJvcnRFcnJvciIsInN1cHBvcnRlZFNjaGVtYXMiLCJvcHRpb25zXyIsImNvbnRlbnRMZW5ndGhWYWx1ZSIsImdldExlbmd0aFN5bmMiLCJoYXNLbm93bkxlbmd0aCIsImdldEZvcm1EYXRhTGVuZ3RoIiwiZ2V0VG90YWxCeXRlcyIsInNlYXJjaCIsImxhc3RPZmZzZXQiLCJocmVmIiwiaGFzaCIsImdldFNlYXJjaCIsInBhdGgiLCJwYXRobmFtZSIsImhvc3RuYW1lIiwicHJvdG9jb2wiLCJwb3J0IiwiZ2V0Tm9kZVJlcXVlc3RPcHRpb25zIiwic2VuZCIsImVtaXQiLCJhYm9ydEFuZEZpbmFsaXplIiwiZmluYWxpemUiLCJyZXF1ZXN0XyIsInJlbW92ZUV2ZW50TGlzdGVuZXIiLCJyZXNwb25zZV8iLCJpbmRleCIsImFycmF5IiwiZnJvbVJhd0hlYWRlcnMiLCJyYXdIZWFkZXJzIiwic3RhdHVzQ29kZSIsImxvY2F0aW9uVVJMIiwicmVxdWVzdE9wdGlvbnMiLCJwaXBlbGluZSIsInByb2Nlc3MiLCJ2ZXJzaW9uIiwicmVzcG9uc2VPcHRpb25zIiwic3RhdHVzTWVzc2FnZSIsImNvZGluZ3MiLCJ6bGliT3B0aW9ucyIsImZsdXNoIiwiWl9TWU5DX0ZMVVNIIiwiZmluaXNoRmx1c2giLCJjcmVhdGVHdW56aXAiLCJjcmVhdGVCcm90bGlEZWNvbXByZXNzIiwiY3JlYXRlSW5mbGF0ZSIsImNyZWF0ZUluZmxhdGVSYXciLCJkZXN0Iiwid3JpdGUiLCJ3cml0ZVRvU3RyZWFtIiwibm9ybWFsaXplIiwiam9pbmVkIiwiU3ltYm9sUG9seWZpbGwiLCJkZXNjcmlwdGlvbiIsIm5vb3AiLCJ0eXBlSXNPYmplY3QiLCJyZXRocm93QXNzZXJ0aW9uRXJyb3JSZWplY3Rpb24iLCJvcmlnaW5hbFByb21pc2UiLCJvcmlnaW5hbFByb21pc2VUaGVuIiwib3JpZ2luYWxQcm9taXNlUmVzb2x2ZSIsIm9yaWdpbmFsUHJvbWlzZVJlamVjdCIsIm5ld1Byb21pc2UiLCJleGVjdXRvciIsInByb21pc2VSZXNvbHZlZFdpdGgiLCJwcm9taXNlUmVqZWN0ZWRXaXRoIiwicmVhc29uIiwiUGVyZm9ybVByb21pc2VUaGVuIiwicHJvbWlzZSIsIm9uRnVsZmlsbGVkIiwib25SZWplY3RlZCIsInVwb25Qcm9taXNlIiwidXBvbkZ1bGZpbGxtZW50IiwidXBvblJlamVjdGlvbiIsInRyYW5zZm9ybVByb21pc2VXaXRoIiwiZnVsZmlsbG1lbnRIYW5kbGVyIiwicmVqZWN0aW9uSGFuZGxlciIsInNldFByb21pc2VJc0hhbmRsZWRUb1RydWUiLCJxdWV1ZU1pY3JvdGFzayIsImdsb2JhbFF1ZXVlTWljcm90YXNrIiwicmVzb2x2ZWRQcm9taXNlIiwicmVmbGVjdENhbGwiLCJGIiwiViIsImFyZ3MiLCJGdW5jdGlvbiIsInByb21pc2VDYWxsIiwiU2ltcGxlUXVldWUiLCJfY3Vyc29yIiwiX3NpemUiLCJfZnJvbnQiLCJfZWxlbWVudHMiLCJfbmV4dCIsIl9iYWNrIiwib2xkQmFjayIsIm5ld0JhY2siLCJRVUVVRV9NQVhfQVJSQVlfU0laRSIsIm9sZEZyb250IiwibmV3RnJvbnQiLCJvbGRDdXJzb3IiLCJuZXdDdXJzb3IiLCJlbGVtZW50cyIsImZyb250IiwiY3Vyc29yIiwiUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljSW5pdGlhbGl6ZSIsIl9vd25lclJlYWRhYmxlU3RyZWFtIiwiX3JlYWRlciIsIl9zdGF0ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSIsImRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVzb2x2ZWQiLCJkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkIiwiX3N0b3JlZEVycm9yIiwiUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsIiwiUmVhZGFibGVTdHJlYW1DYW5jZWwiLCJSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlIiwiZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZWplY3QiLCJkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlc2V0VG9SZWplY3RlZCIsInJlYWRlckxvY2tFeGNlcHRpb24iLCJfY2xvc2VkUHJvbWlzZSIsIl9jbG9zZWRQcm9taXNlX3Jlc29sdmUiLCJfY2xvc2VkUHJvbWlzZV9yZWplY3QiLCJBYm9ydFN0ZXBzIiwiRXJyb3JTdGVwcyIsIkNhbmNlbFN0ZXBzIiwiUHVsbFN0ZXBzIiwiTnVtYmVySXNGaW5pdGUiLCJpc0Zpbml0ZSIsIk1hdGhUcnVuYyIsInRydW5jIiwidiIsImNlaWwiLCJmbG9vciIsImFzc2VydERpY3Rpb25hcnkiLCJjb250ZXh0IiwiYXNzZXJ0RnVuY3Rpb24iLCJhc3NlcnRPYmplY3QiLCJhc3NlcnRSZXF1aXJlZEFyZ3VtZW50IiwicG9zaXRpb24iLCJhc3NlcnRSZXF1aXJlZEZpZWxkIiwiY29udmVydFVucmVzdHJpY3RlZERvdWJsZSIsImNlbnNvck5lZ2F0aXZlWmVybyIsImNvbnZlcnRVbnNpZ25lZExvbmdMb25nV2l0aEVuZm9yY2VSYW5nZSIsInVwcGVyQm91bmQiLCJNQVhfU0FGRV9JTlRFR0VSIiwiaW50ZWdlclBhcnQiLCJhc3NlcnRSZWFkYWJsZVN0cmVhbSIsIklzUmVhZGFibGVTdHJlYW0iLCJBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIiwiUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdCIsInJlYWRSZXF1ZXN0IiwiX3JlYWRSZXF1ZXN0cyIsIlJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0Iiwic2hpZnQiLCJfY2xvc2VTdGVwcyIsIl9jaHVua1N0ZXBzIiwiUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkUmVxdWVzdHMiLCJSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIiLCJJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciIsIklzUmVhZGFibGVTdHJlYW1Mb2NrZWQiLCJkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsInJlc29sdmVQcm9taXNlIiwicmVqZWN0UHJvbWlzZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlclJlYWQiLCJfZXJyb3JTdGVwcyIsImUiLCJoYXNPd25Qcm9wZXJ0eSIsIl9kaXN0dXJiZWQiLCJfcmVhZGFibGVTdHJlYW1Db250cm9sbGVyIiwiY2FuY2VsIiwicmVsZWFzZUxvY2siLCJjbG9zZWQiLCJBc3luY0l0ZXJhdG9yUHJvdG90eXBlIiwiUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9ySW1wbCIsInByZXZlbnRDYW5jZWwiLCJfb25nb2luZ1Byb21pc2UiLCJfaXNGaW5pc2hlZCIsIl9wcmV2ZW50Q2FuY2VsIiwibmV4dFN0ZXBzIiwiX25leHRTdGVwcyIsInJldHVyblN0ZXBzIiwiX3JldHVyblN0ZXBzIiwiUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yUHJvdG90eXBlIiwiSXNSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IiLCJfYXN5bmNJdGVyYXRvckltcGwiLCJzdHJlYW1Bc3luY0l0ZXJhdG9yQnJhbmRDaGVja0V4Y2VwdGlvbiIsInJldHVybiIsIk51bWJlcklzTmFOIiwiSXNGaW5pdGVOb25OZWdhdGl2ZU51bWJlciIsIklzTm9uTmVnYXRpdmVOdW1iZXIiLCJJbmZpbml0eSIsIkRlcXVldWVWYWx1ZSIsImNvbnRhaW5lciIsIl9xdWV1ZSIsIl9xdWV1ZVRvdGFsU2l6ZSIsIkVucXVldWVWYWx1ZVdpdGhTaXplIiwiUmVzZXRRdWV1ZSIsIkNyZWF0ZUFycmF5RnJvbUxpc3QiLCJSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0IiwiSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0IiwiYnlvYlJlcXVlc3RCcmFuZENoZWNrRXhjZXB0aW9uIiwiX3ZpZXciLCJieXRlc1dyaXR0ZW4iLCJfYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEludGVybmFsIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmQiLCJ2aWV3IiwiZmlyc3REZXNjcmlwdG9yIiwiX3BlbmRpbmdQdWxsSW50b3MiLCJwZWVrIiwiYnl0ZXNGaWxsZWQiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZFdpdGhOZXdWaWV3IiwicmVzcG9uZCIsInJlc3BvbmRXaXRoTmV3VmlldyIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIiLCJieXRlU3RyZWFtQ29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24iLCJfYnlvYlJlcXVlc3QiLCJieW9iUmVxdWVzdCIsIlNldFVwUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdCIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSIsIl9jbG9zZVJlcXVlc3RlZCIsIl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVycm9yIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIlJlYWRhYmxlU3RyZWFtQ2xvc2UiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2xvc2UiLCJ0cmFuc2ZlcnJlZEJ1ZmZlciIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlQ2h1bmtUb1F1ZXVlIiwiUmVhZGFibGVTdHJlYW1IYXNCWU9CUmVhZGVyIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclByb2Nlc3NQdWxsSW50b0Rlc2NyaXB0b3JzVXNpbmdRdWV1ZSIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWUiLCJfY2FuY2VsQWxnb3JpdGhtIiwiZW50cnkiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbiIsImF1dG9BbGxvY2F0ZUNodW5rU2l6ZSIsIl9hdXRvQWxsb2NhdGVDaHVua1NpemUiLCJidWZmZXJFIiwicHVsbEludG9EZXNjcmlwdG9yIiwiZWxlbWVudFNpemUiLCJ2aWV3Q29uc3RydWN0b3IiLCJyZWFkZXJUeXBlIiwiX3N0YXJ0ZWQiLCJSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRJbnRvUmVxdWVzdHMiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hvdWxkQ2FsbFB1bGwiLCJfcHVsbGluZyIsIl9wdWxsQWdhaW4iLCJfcHVsbEFsZ29yaXRobSIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb21taXRQdWxsSW50b0Rlc2NyaXB0b3IiLCJmaWxsZWRWaWV3IiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNvbnZlcnRQdWxsSW50b0Rlc2NyaXB0b3IiLCJyZWFkSW50b1JlcXVlc3QiLCJfcmVhZEludG9SZXF1ZXN0cyIsIlJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRJbnRvUmVxdWVzdCIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsUHVsbEludG9EZXNjcmlwdG9yRnJvbVF1ZXVlIiwiY3VycmVudEFsaWduZWRCeXRlcyIsIm1heEJ5dGVzVG9Db3B5IiwibWF4Qnl0ZXNGaWxsZWQiLCJtYXhBbGlnbmVkQnl0ZXMiLCJ0b3RhbEJ5dGVzVG9Db3B5UmVtYWluaW5nIiwicmVhZHkiLCJxdWV1ZSIsImhlYWRPZlF1ZXVlIiwiYnl0ZXNUb0NvcHkiLCJkZXN0U3RhcnQiLCJkZXN0T2Zmc2V0Iiwic3JjIiwic3JjT2Zmc2V0IiwibiIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsSGVhZFB1bGxJbnRvRGVzY3JpcHRvciIsIlJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJJbnZhbGlkYXRlQllPQlJlcXVlc3QiLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8iLCJSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEluQ2xvc2VkU3RhdGUiLCJyZW1haW5kZXJTaXplIiwicmVtYWluZGVyIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJblJlYWRhYmxlU3RhdGUiLCJkZXNjcmlwdG9yIiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyUGVuZGluZ1B1bGxJbnRvcyIsIlJlYWRhYmxlU3RyZWFtRXJyb3IiLCJfc3RyYXRlZ3lIV00iLCJSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdCIsIklzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyIiwiZGVzaXJlZFNpemUiLCJSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIiLCJieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsIkRhdGFWaWV3IiwiQllURVNfUEVSX0VMRU1FTlQiLCJjdG9yIiwiZW1wdHlWaWV3IiwiUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclB1bGxJbnRvIiwiUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyUmVhZCIsIkV4dHJhY3RIaWdoV2F0ZXJNYXJrIiwic3RyYXRlZ3kiLCJkZWZhdWx0SFdNIiwiRXh0cmFjdFNpemVBbGdvcml0aG0iLCJjb252ZXJ0UXVldWluZ1N0cmF0ZWd5IiwiY29udmVydFF1ZXVpbmdTdHJhdGVneVNpemUiLCJjb252ZXJ0VW5kZXJseWluZ1NpbmtBYm9ydENhbGxiYWNrIiwib3JpZ2luYWwiLCJjb252ZXJ0VW5kZXJseWluZ1NpbmtDbG9zZUNhbGxiYWNrIiwiY29udmVydFVuZGVybHlpbmdTaW5rU3RhcnRDYWxsYmFjayIsImNvbnZlcnRVbmRlcmx5aW5nU2lua1dyaXRlQ2FsbGJhY2siLCJhc3NlcnRXcml0YWJsZVN0cmVhbSIsIklzV3JpdGFibGVTdHJlYW0iLCJXcml0YWJsZVN0cmVhbSIsInJhd1VuZGVybHlpbmdTaW5rIiwicmF3U3RyYXRlZ3kiLCJ1bmRlcmx5aW5nU2luayIsImNvbnZlcnRVbmRlcmx5aW5nU2luayIsIkluaXRpYWxpemVXcml0YWJsZVN0cmVhbSIsInNpemVBbGdvcml0aG0iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyIiwic3RhcnRBbGdvcml0aG0iLCJ3cml0ZUFsZ29yaXRobSIsImNsb3NlQWxnb3JpdGhtIiwiYWJvcnRBbGdvcml0aG0iLCJTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIiLCJTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NpbmsiLCJzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uIiwiSXNXcml0YWJsZVN0cmVhbUxvY2tlZCIsIldyaXRhYmxlU3RyZWFtQWJvcnQiLCJXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodCIsIldyaXRhYmxlU3RyZWFtQ2xvc2UiLCJBY3F1aXJlV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIiwiX3dyaXRlciIsIl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIiLCJfd3JpdGVSZXF1ZXN0cyIsIl9pbkZsaWdodFdyaXRlUmVxdWVzdCIsIl9jbG9zZVJlcXVlc3QiLCJfaW5GbGlnaHRDbG9zZVJlcXVlc3QiLCJfcGVuZGluZ0Fib3J0UmVxdWVzdCIsIl9iYWNrcHJlc3N1cmUiLCJfcHJvbWlzZSIsIndhc0FscmVhZHlFcnJvcmluZyIsIl9yZXNvbHZlIiwiX3JlamVjdCIsIl9yZWFzb24iLCJfd2FzQWxyZWFkeUVycm9yaW5nIiwiV3JpdGFibGVTdHJlYW1TdGFydEVycm9yaW5nIiwiY2xvc2VSZXF1ZXN0Iiwid3JpdGVyIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlc29sdmUiLCJjbG9zZVNlbnRpbmVsIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkIiwiV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbiIsIldyaXRhYmxlU3RyZWFtRmluaXNoRXJyb3JpbmciLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJFbnN1cmVSZWFkeVByb21pc2VSZWplY3RlZCIsIldyaXRhYmxlU3RyZWFtSGFzT3BlcmF0aW9uTWFya2VkSW5GbGlnaHQiLCJzdG9yZWRFcnJvciIsIndyaXRlUmVxdWVzdCIsIldyaXRhYmxlU3RyZWFtUmVqZWN0Q2xvc2VBbmRDbG9zZWRQcm9taXNlSWZOZWVkZWQiLCJhYm9ydFJlcXVlc3QiLCJkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCIsIldyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlIiwiYmFja3ByZXNzdXJlIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUiLCJkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQiLCJnZXRXcml0ZXIiLCJsb2NrZWQiLCJfb3duZXJXcml0YWJsZVN0cmVhbSIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQiLCJJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciIsImRlZmF1bHRXcml0ZXJCcmFuZENoZWNrRXhjZXB0aW9uIiwiZGVmYXVsdFdyaXRlckxvY2tFeGNlcHRpb24iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJHZXREZXNpcmVkU2l6ZSIsIl9yZWFkeVByb21pc2UiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJBYm9ydCIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckNsb3NlIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcldyaXRlIiwiX3JlYWR5UHJvbWlzZVN0YXRlIiwiZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlamVjdCIsImRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNldFRvUmVqZWN0ZWQiLCJyZWxlYXNlZEVycm9yIiwiX2Nsb3NlZFByb21pc2VTdGF0ZSIsImRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlQ2xvc2VkUHJvbWlzZVJlamVjdGVkIiwiY2h1bmtTaXplIiwiX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSIsImNodW5rU2l6ZUUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZCIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRDaHVua1NpemUiLCJXcml0YWJsZVN0cmVhbUFkZFdyaXRlUmVxdWVzdCIsImVucXVldWVFIiwiX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbSIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRCYWNrcHJlc3N1cmUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyV3JpdGUiLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IiLCJfYWJvcnRBbGdvcml0aG0iLCJXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zIiwiX3dyaXRlQWxnb3JpdGhtIiwiX2Nsb3NlQWxnb3JpdGhtIiwiciIsIldyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0Iiwic2lua0Nsb3NlUHJvbWlzZSIsIldyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZSIsIldyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZVdpdGhFcnJvciIsIldyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQcm9jZXNzQ2xvc2UiLCJXcml0YWJsZVN0cmVhbU1hcmtGaXJzdFdyaXRlUmVxdWVzdEluRmxpZ2h0IiwiV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlIiwiV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlV2l0aEVycm9yIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NXcml0ZSIsIl9yZWFkeVByb21pc2VfcmVzb2x2ZSIsIl9yZWFkeVByb21pc2VfcmVqZWN0IiwiTmF0aXZlRE9NRXhjZXB0aW9uIiwiRE9NRXhjZXB0aW9uIiwiRE9NRXhjZXB0aW9uJDEiLCJfYSIsImlzRE9NRXhjZXB0aW9uQ29uc3RydWN0b3IiLCJjcmVhdGVET01FeGNlcHRpb25Qb2x5ZmlsbCIsIlJlYWRhYmxlU3RyZWFtUGlwZVRvIiwicHJldmVudENsb3NlIiwicHJldmVudEFib3J0Iiwic2h1dHRpbmdEb3duIiwiY3VycmVudFdyaXRlIiwiYWN0aW9ucyIsInNodXRkb3duV2l0aEFjdGlvbiIsImFsbCIsImFjdGlvbiIsImlzT3JCZWNvbWVzRXJyb3JlZCIsInNodXRkb3duIiwiV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyQ2xvc2VXaXRoRXJyb3JQcm9wYWdhdGlvbiIsImRlc3RDbG9zZWQiLCJ3YWl0Rm9yV3JpdGVzVG9GaW5pc2giLCJvbGRDdXJyZW50V3JpdGUiLCJvcmlnaW5hbElzRXJyb3IiLCJvcmlnaW5hbEVycm9yIiwiZG9UaGVSZXN0IiwibmV3RXJyb3IiLCJpc0Vycm9yIiwicmVzb2x2ZUxvb3AiLCJyZWplY3RMb29wIiwicmVzb2x2ZVJlYWQiLCJyZWplY3RSZWFkIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsIklzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsImRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZSIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW0iLCJSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZCIsIlJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbCIsIlNldFVwUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsInB1bGxBbGdvcml0aG0iLCJjYW5jZWxBbGdvcml0aG0iLCJjb252ZXJ0VW5kZXJseWluZ1NvdXJjZUNhbmNlbENhbGxiYWNrIiwiY29udmVydFVuZGVybHlpbmdTb3VyY2VQdWxsQ2FsbGJhY2siLCJjb252ZXJ0VW5kZXJseWluZ1NvdXJjZVN0YXJ0Q2FsbGJhY2siLCJjb252ZXJ0UmVhZGFibGVTdHJlYW1UeXBlIiwiY29udmVydFJlYWRhYmxlU3RyZWFtUmVhZGVyTW9kZSIsIm1vZGUiLCJjb252ZXJ0UGlwZU9wdGlvbnMiLCJpc0Fib3J0U2lnbmFsIiwiYXNzZXJ0QWJvcnRTaWduYWwiLCJyYXdVbmRlcmx5aW5nU291cmNlIiwidW5kZXJseWluZ1NvdXJjZSIsInB1bGwiLCJjb252ZXJ0VW5kZXJseWluZ0RlZmF1bHRPckJ5dGVTb3VyY2UiLCJJbml0aWFsaXplUmVhZGFibGVTdHJlYW0iLCJ1bmRlcmx5aW5nQnl0ZVNvdXJjZSIsIlNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciIsIlNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZyb21VbmRlcmx5aW5nU291cmNlIiwiU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTb3VyY2UiLCJzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEiLCJyYXdPcHRpb25zIiwiY29udmVydFJlYWRlck9wdGlvbnMiLCJyYXdUcmFuc2Zvcm0iLCJ0cmFuc2Zvcm0iLCJyZWFkYWJsZSIsImNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpciIsImRlc3RpbmF0aW9uIiwiYnJhbmNoZXMiLCJjbG9uZUZvckJyYW5jaDIiLCJyZWFzb24xIiwicmVhc29uMiIsImJyYW5jaDEiLCJicmFuY2gyIiwicmVzb2x2ZUNhbmNlbFByb21pc2UiLCJyZWFkaW5nIiwiY2FuY2VsZWQxIiwiY2FuY2VsZWQyIiwiY2FuY2VsUHJvbWlzZSIsInZhbHVlMSIsInZhbHVlMiIsIkNyZWF0ZVJlYWRhYmxlU3RyZWFtIiwiY29tcG9zaXRlUmVhc29uIiwiY2FuY2VsUmVzdWx0IiwiUmVhZGFibGVTdHJlYW1UZWUiLCJpbXBsIiwiQWNxdWlyZVJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvciIsImNvbnZlcnRJdGVyYXRvck9wdGlvbnMiLCJjb252ZXJ0UXVldWluZ1N0cmF0ZWd5SW5pdCIsInBpcGVUaHJvdWdoIiwicGlwZVRvIiwidGVlIiwiYXN5bmNJdGVyYXRvciIsImJ5dGVMZW5ndGhTaXplRnVuY3Rpb24iLCJCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IiwiX2J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrIiwiSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IiwiYnl0ZUxlbmd0aEJyYW5kQ2hlY2tFeGNlcHRpb24iLCJjb3VudFNpemVGdW5jdGlvbiIsIkNvdW50UXVldWluZ1N0cmF0ZWd5IiwiX2NvdW50UXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyayIsIklzQ291bnRRdWV1aW5nU3RyYXRlZ3kiLCJjb3VudEJyYW5kQ2hlY2tFeGNlcHRpb24iLCJjb252ZXJ0VHJhbnNmb3JtZXJGbHVzaENhbGxiYWNrIiwiY29udmVydFRyYW5zZm9ybWVyU3RhcnRDYWxsYmFjayIsImNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrIiwiVHJhbnNmb3JtU3RyZWFtIiwicmF3VHJhbnNmb3JtZXIiLCJyYXdXcml0YWJsZVN0cmF0ZWd5IiwicmF3UmVhZGFibGVTdHJhdGVneSIsIndyaXRhYmxlU3RyYXRlZ3kiLCJyZWFkYWJsZVN0cmF0ZWd5IiwidHJhbnNmb3JtZXIiLCJyZWFkYWJsZVR5cGUiLCJ3cml0YWJsZVR5cGUiLCJjb252ZXJ0VHJhbnNmb3JtZXIiLCJyZWFkYWJsZUhpZ2hXYXRlck1hcmsiLCJyZWFkYWJsZVNpemVBbGdvcml0aG0iLCJ3cml0YWJsZUhpZ2hXYXRlck1hcmsiLCJ3cml0YWJsZVNpemVBbGdvcml0aG0iLCJzdGFydFByb21pc2VfcmVzb2x2ZSIsInN0YXJ0UHJvbWlzZSIsIl93cml0YWJsZSIsIkNyZWF0ZVdyaXRhYmxlU3RyZWFtIiwiX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXIiLCJfYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyUGVyZm9ybVRyYW5zZm9ybSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rV3JpdGVBbGdvcml0aG0iLCJfcmVhZGFibGUiLCJmbHVzaFByb21pc2UiLCJfZmx1c2hBbGdvcml0aG0iLCJUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyIsIlRyYW5zZm9ybVN0cmVhbUVycm9yIiwiVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtDbG9zZUFsZ29yaXRobSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQWJvcnRBbGdvcml0aG0iLCJUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUiLCJUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0U291cmNlUHVsbEFsZ29yaXRobSIsIlRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUiLCJfYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZV9yZXNvbHZlIiwiSW5pdGlhbGl6ZVRyYW5zZm9ybVN0cmVhbSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyIiwidHJhbnNmb3JtQWxnb3JpdGhtIiwiVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlIiwidHJhbnNmb3JtUmVzdWx0RSIsImZsdXNoQWxnb3JpdGhtIiwiX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0iLCJfdHJhbnNmb3JtQWxnb3JpdGhtIiwiU2V0VXBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciIsIlNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVHJhbnNmb3JtZXIiLCJJc1RyYW5zZm9ybVN0cmVhbSIsInN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMiIsIklzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIiLCJkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24kMSIsIlRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyVGVybWluYXRlIiwicmVhZGFibGVDb250cm9sbGVyIiwiUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZSIsInRlcm1pbmF0ZSIsInJlcXVpcmUiLCJfX3dlYnBhY2tfbW9kdWxlX2NhY2hlX18iLCJfX3dlYnBhY2tfcmVxdWlyZV9fIiwibW9kdWxlSWQiLCJfX3dlYnBhY2tfbW9kdWxlc19fIiwiZGVmaW5pdGlvbiIsIm8iLCJwcm9wIl0sIm1hcHBpbmdzIjoiO0NBQUEsU0FBMkNBLEVBQU1DLEdBQzFCLGlCQUFaQyxTQUEwQyxpQkFBWEMsT0FDeENBLE9BQU9ELFFBQVVELElBQ1EsbUJBQVhHLFFBQXlCQSxPQUFPQyxJQUM5Q0QsT0FBTyxHQUFJSCxHQUNlLGlCQUFaQyxRQUNkQSxRQUFpQixRQUFJRCxJQUVyQkQsRUFBYyxRQUFJQyxJQVJwQixDQVNHSyxNQUFNLFdBQ1QsTSx3Q0NKQUMsT0FBT0MsZUFBZU4sRUFBUyxhQUEvQixDQUErQ08sT0FBTyxJQUV0RCxJQUFJQyxFQUFrQixFQUFRLEtBTTlCLE1BQU1DLFVBQW9CRCxFQUFnQkUsWUFJdEMs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T0FBT1ksVUFBVWtxQixlQUFldHBCLEtBQUtpSixFQUFLaTZCLEdDQ2pGTCxFQUFvQjlLLEVBQUs1NUIsSUFDSCxvQkFBWHFCLFFBQTBCQSxPQUFPQyxhQUMxQ2pCLE9BQU9DLGVBQWVOLEVBQVNxQixPQUFPQyxZQUFhLENBQUVmLE1BQU8sV0FFN0RGLE9BQU9DLGVBQWVOLEVBQVMsYUFBYyxDQUFFTyxPQUFPLEtIRmhEbWtDLEVBQW9CLE0iLCJmaWxlIjoibWFpbGd1bi5qcyIsInNvdXJjZXNDb250ZW50IjpbIihmdW5jdGlvbiB3ZWJwYWNrVW5pdmVyc2FsTW9kdWxlRGVmaW5pdGlvbihyb290LCBmYWN0b3J5KSB7XG5cdGlmKHR5cGVvZiBleHBvcnRzID09PSAnb2JqZWN0JyAmJiB0eXBlb2YgbW9kdWxlID09PSAnb2JqZWN0Jylcblx0XHRtb2R1bGUuZXhwb3J0cyA9IGZhY3RvcnkoKTtcblx0ZWxzZSBpZih0eXBlb2YgZGVmaW5lID09PSAnZnVuY3Rpb24nICYmIGRlZmluZS5hbWQpXG5cdFx0ZGVmaW5lKFtdLCBmYWN0b3J5KTtcblx0ZWxzZSBpZih0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcpXG5cdFx0ZXhwb3J0c1tcIm1haWxndW5cIl0gPSBmYWN0b3J5KCk7XG5cdGVsc2Vcblx0XHRyb290W1wibWFpbGd1blwiXSA9IGZhY3RvcnkoKTtcbn0pKHRoaXMsIGZ1bmN0aW9uKCkge1xucmV0dXJuICIsIi8qKlxuICogQGF1dGhvciBUb3J1IE5hZ2FzaGltYSA8aHR0cHM6Ly9naXRodWIuY29tL215c3RpY2F0ZWE+XG4gKiBTZWUgTElDRU5TRSBmaWxlIGluIHJvb3QgZGlyZWN0b3J5IGZvciBmdWxsIGxpY2Vuc2UuXG4gKi9cbid1c2Ugc3RyaWN0JztcblxuT2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywgeyB2YWx1ZTogdHJ1ZSB9KTtcblxudmFyIGV2ZW50VGFyZ2V0U2hpbSA9IHJlcXVpcmUoJ2V2ZW50LXRhcmdldC1zaGltJyk7XG5cbi8qKlxuICogVGhlIHNpZ25hbCBjbGFzcy5cbiAqIEBzZWUgaHR0cHM6Ly9kb20uc3BlYy53aGF0d2cub3JnLyNhYm9ydHNpZ25hbFxuICovXG5jbGFzcyBBYm9ydFNpZ25hbCBleHRlbmRzIGV2ZW50VGFyZ2V0U2hpbS5FdmVudFRhcmdldCB7XG4gICAgLyoqXG4gICAgICogQWJvcnRTaWduYWwgY2Fubm90IGJlIGNvbnN0cnVjdGVkIGRpcmVjdGx5LlxuICAgICAqL1xuICAgIGNvbnN0cnVjdG9yKCkge1xuICAgICAgICBzdXBlcigpO1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKFwiQWJvcnRTaWduYWwgY2Fubm90IGJlIGNvbnN0cnVjdGVkIGRpcmVjdGx5XCIpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGB0cnVlYCBpZiB0aGlzIGBBYm9ydFNpZ25hbGAncyBgQWJvcnRDb250cm9sbGVyYCBoYXMgc2lnbmFsZWQgdG8gYWJvcnQsIGFuZCBgZmFsc2VgIG90aGVyd2lzZS5cbiAgICAgKi9cbiAgICBnZXQgYWJvcnRlZCgpIHtcbiAgICAgICAgY29uc3QgYWJvcnRlZCA9IGFib3J0ZWRGbGFncy5nZXQodGhpcyk7XG4gICAgICAgIGlmICh0eXBlb2YgYWJvcnRlZCAhPT0gXCJib29sZWFuXCIpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYEV4cGVjdGVkICd0aGlzJyB0byBiZSBhbiAnQWJvcnRTaWduYWwnIG9iamVjdCwgYnV0IGdvdCAke3RoaXMgPT09IG51bGwgPyBcIm51bGxcIiA6IHR5cGVvZiB0aGlzfWApO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBhYm9ydGVkO1xuICAgIH1cbn1cbmV2ZW50VGFyZ2V0U2hpbS5kZWZpbmVFdmVudEF0dHJpYnV0ZShBYm9ydFNpZ25hbC5wcm90b3R5cGUsIFwiYWJvcnRcIik7XG4vKipcbiAqIENyZWF0ZSBhbiBBYm9ydFNpZ25hbCBvYmplY3QuXG4gKi9cbmZ1bmN0aW9uIGNyZWF0ZUFib3J0U2lnbmFsKCkge1xuICAgIGNvbnN0IHNpZ25hbCA9IE9iamVjdC5jcmVhdGUoQWJvcnRTaWduYWwucHJvdG90eXBlKTtcbiAgICBldmVudFRhcmdldFNoaW0uRXZlbnRUYXJnZXQuY2FsbChzaWduYWwpO1xuICAgIGFib3J0ZWRGbGFncy5zZXQoc2lnbmFsLCBmYWxzZSk7XG4gICAgcmV0dXJuIHNpZ25hbDtcbn1cbi8qKlxuICogQWJvcnQgYSBnaXZlbiBzaWduYWwuXG4gKi9cbmZ1bmN0aW9uIGFib3J0U2lnbmFsKHNpZ25hbCkge1xuICAgIGlmIChhYm9ydGVkRmxhZ3MuZ2V0KHNpZ25hbCkgIT09IGZhbHNlKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgYWJvcnRlZEZsYWdzLnNldChzaWduYWwsIHRydWUpO1xuICAgIHNpZ25hbC5kaXNwYXRjaEV2ZW50KHsgdHlwZTogXCJhYm9ydFwiIH0pO1xufVxuLyoqXG4gKiBBYm9ydGVkIGZsYWcgZm9yIGVhY2ggaW5zdGFuY2VzLlxuICovXG5jb25zdCBhYm9ydGVkRmxhZ3MgPSBuZXcgV2Vha01hcCgpO1xuLy8gUHJvcGVydGllcyBzaG91bGQgYmUgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEFib3J0U2lnbmFsLnByb3RvdHlwZSwge1xuICAgIGFib3J0ZWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxufSk7XG4vLyBgdG9TdHJpbmcoKWAgc2hvdWxkIHJldHVybiBgXCJbb2JqZWN0IEFib3J0U2lnbmFsXVwiYFxuaWYgKHR5cGVvZiBTeW1ib2wgPT09IFwiZnVuY3Rpb25cIiAmJiB0eXBlb2YgU3ltYm9sLnRvU3RyaW5nVGFnID09PSBcInN5bWJvbFwiKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KEFib3J0U2lnbmFsLnByb3RvdHlwZSwgU3ltYm9sLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgdmFsdWU6IFwiQWJvcnRTaWduYWxcIixcbiAgICB9KTtcbn1cblxuLyoqXG4gKiBUaGUgQWJvcnRDb250cm9sbGVyLlxuICogQHNlZSBodHRwczovL2RvbS5zcGVjLndoYXR3Zy5vcmcvI2Fib3J0Y29udHJvbGxlclxuICovXG5jbGFzcyBBYm9ydENvbnRyb2xsZXIge1xuICAgIC8qKlxuICAgICAqIEluaXRpYWxpemUgdGhpcyBjb250cm9sbGVyLlxuICAgICAqL1xuICAgIGNvbnN0cnVjdG9yKCkge1xuICAgICAgICBzaWduYWxzLnNldCh0aGlzLCBjcmVhdGVBYm9ydFNpZ25hbCgpKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgYEFib3J0U2lnbmFsYCBvYmplY3QgYXNzb2NpYXRlZCB3aXRoIHRoaXMgb2JqZWN0LlxuICAgICAqL1xuICAgIGdldCBzaWduYWwoKSB7XG4gICAgICAgIHJldHVybiBnZXRTaWduYWwodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIEFib3J0IGFuZCBzaWduYWwgdG8gYW55IG9ic2VydmVycyB0aGF0IHRoZSBhc3NvY2lhdGVkIGFjdGl2aXR5IGlzIHRvIGJlIGFib3J0ZWQuXG4gICAgICovXG4gICAgYWJvcnQoKSB7XG4gICAgICAgIGFib3J0U2lnbmFsKGdldFNpZ25hbCh0aGlzKSk7XG4gICAgfVxufVxuLyoqXG4gKiBBc3NvY2lhdGVkIHNpZ25hbHMuXG4gKi9cbmNvbnN0IHNpZ25hbHMgPSBuZXcgV2Vha01hcCgpO1xuLyoqXG4gKiBHZXQgdGhlIGFzc29jaWF0ZWQgc2lnbmFsIG9mIGEgZ2l2ZW4gY29udHJvbGxlci5cbiAqL1xuZnVuY3Rpb24gZ2V0U2lnbmFsKGNvbnRyb2xsZXIpIHtcbiAgICBjb25zdCBzaWduYWwgPSBzaWduYWxzLmdldChjb250cm9sbGVyKTtcbiAgICBpZiAoc2lnbmFsID09IG51bGwpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgRXhwZWN0ZWQgJ3RoaXMnIHRvIGJlIGFuICdBYm9ydENvbnRyb2xsZXInIG9iamVjdCwgYnV0IGdvdCAke2NvbnRyb2xsZXIgPT09IG51bGwgPyBcIm51bGxcIiA6IHR5cGVvZiBjb250cm9sbGVyfWApO1xuICAgIH1cbiAgICByZXR1cm4gc2lnbmFsO1xufVxuLy8gUHJvcGVydGllcyBzaG91bGQgYmUgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEFib3J0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBzaWduYWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGFib3J0OiB7IGVudW1lcmFibGU6IHRydWUgfSxcbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2wgPT09IFwiZnVuY3Rpb25cIiAmJiB0eXBlb2YgU3ltYm9sLnRvU3RyaW5nVGFnID09PSBcInN5bWJvbFwiKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KEFib3J0Q29udHJvbGxlci5wcm90b3R5cGUsIFN5bWJvbC50b1N0cmluZ1RhZywge1xuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIHZhbHVlOiBcIkFib3J0Q29udHJvbGxlclwiLFxuICAgIH0pO1xufVxuXG5leHBvcnRzLkFib3J0Q29udHJvbGxlciA9IEFib3J0Q29udHJvbGxlcjtcbmV4cG9ydHMuQWJvcnRTaWduYWwgPSBBYm9ydFNpZ25hbDtcbmV4cG9ydHMuZGVmYXVsdCA9IEFib3J0Q29udHJvbGxlcjtcblxubW9kdWxlLmV4cG9ydHMgPSBBYm9ydENvbnRyb2xsZXJcbm1vZHVsZS5leHBvcnRzLkFib3J0Q29udHJvbGxlciA9IG1vZHVsZS5leHBvcnRzW1wiZGVmYXVsdFwiXSA9IEFib3J0Q29udHJvbGxlclxubW9kdWxlLmV4cG9ydHMuQWJvcnRTaWduYWwgPSBBYm9ydFNpZ25hbFxuLy8jIHNvdXJjZU1hcHBpbmdVUkw9YWJvcnQtY29udHJvbGxlci5qcy5tYXBcbiIsImltcG9ydCBDbGllbnQgZnJvbSAnLi9saWIvY2xpZW50J1xuaW1wb3J0IE9wdGlvbnMgZnJvbSAnLi9saWIvaW50ZXJmYWNlcy9PcHRpb25zJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgTWFpbGd1biB7XG4gIHByaXZhdGUgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YVxuXG4gIGNvbnN0cnVjdG9yKEZvcm1EYXRhOiBuZXcgKCkgPT4gRm9ybURhdGEpIHtcbiAgICB0aGlzLmZvcm1EYXRhID0gRm9ybURhdGE7XG4gIH1cblxuICBjbGllbnQob3B0aW9uczogT3B0aW9ucykge1xuICAgIHJldHVybiBuZXcgQ2xpZW50KG9wdGlvbnMsIHRoaXMuZm9ybURhdGEpXG4gIH1cbn07IiwiaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi9yZXF1ZXN0JztcbmltcG9ydCBPcHRpb25zIGZyb20gJy4vaW50ZXJmYWNlcy9PcHRpb25zJztcbmltcG9ydCBSZXF1ZXN0T3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvUmVxdWVzdE9wdGlvbnMnO1xuXG5pbXBvcnQgRG9tYWluQ2xpZW50IGZyb20gJy4vZG9tYWlucyc7XG5pbXBvcnQgRXZlbnRDbGllbnQgZnJvbSAnLi9ldmVudHMnO1xuaW1wb3J0IFN0YXRzQ2xpZW50IGZyb20gJy4vc3RhdHMnO1xuaW1wb3J0IFN1cHByZXNzaW9uQ2xpZW50IGZyb20gJy4vc3VwcHJlc3Npb25zJztcbmltcG9ydCBXZWJob29rQ2xpZW50IGZyb20gJy4vd2ViaG9va3MnO1xuaW1wb3J0IE1lc3NhZ2VzQ2xpZW50IGZyb20gJy4vbWVzc2FnZXMnO1xuaW1wb3J0IFJvdXRlc0NsaWVudCBmcm9tICcuL3JvdXRlcyc7XG5pbXBvcnQgVmFsaWRhdGVDbGllbnQgZnJvbSAnLi92YWxpZGF0ZSc7XG5pbXBvcnQgUGFyc2VDbGllbnQgZnJvbSAnLi9wYXJzZSc7XG5pbXBvcnQgSXBzQ2xpZW50IGZyb20gJy4vaXBzJztcbmltcG9ydCBJcFBvb2xzQ2xpZW50IGZyb20gJy4vaXAtcG9vbHMnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBDbGllbnQge1xuICBwcml2YXRlIHJlcXVlc3Q7XG5cbiAgcHVibGljIGRvbWFpbnM7XG4gIHB1YmxpYyB3ZWJob29rcztcbiAgcHVibGljIGV2ZW50cztcbiAgcHVibGljIHN0YXRzO1xuICBwdWJsaWMgc3VwcHJlc3Npb25zO1xuICBwdWJsaWMgbWVzc2FnZXM7XG4gIHB1YmxpYyByb3V0ZXM7XG4gIHB1YmxpYyBwdWJsaWNfcmVxdWVzdDtcbiAgcHVibGljIHZhbGlkYXRlO1xuICBwdWJsaWMgcGFyc2U7XG4gIHB1YmxpYyBpcHM7XG4gIHB1YmxpYyBpcF9wb29scztcblxuICBjb25zdHJ1Y3RvcihvcHRpb25zOiBPcHRpb25zLCBmb3JtRGF0YTogbmV3ICgpID0+IEZvcm1EYXRhKSB7XG4gICAgY29uc3QgY29uZmlnOiBSZXF1ZXN0T3B0aW9ucyA9IHsgLi4ub3B0aW9ucyB9IGFzIFJlcXVlc3RPcHRpb25zO1xuXG4gICAgaWYgKCFjb25maWcudXJsKSB7XG4gICAgICBjb25maWcudXJsID0gJ2h0dHBzOi8vYXBpLm1haWxndW4ubmV0J1xuICAgIH1cblxuICAgIGlmICghY29uZmlnLnVzZXJuYW1lKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoJ1BhcmFtZXRlciBcInVzZXJuYW1lXCIgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG5cbiAgICBpZiAoIWNvbmZpZy5rZXkpIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcignUGFyYW1ldGVyIFwia2V5XCIgaXMgcmVxdWlyZWQnKTtcbiAgICB9XG5cbiAgICAvKiogQGludGVybmFsICovXG4gICAgdGhpcy5yZXF1ZXN0ID0gbmV3IFJlcXVlc3QoY29uZmlnLCBmb3JtRGF0YSk7XG5cbiAgICB0aGlzLmRvbWFpbnMgPSBuZXcgRG9tYWluQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy53ZWJob29rcyA9IG5ldyBXZWJob29rQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5ldmVudHMgPSBuZXcgRXZlbnRDbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICB0aGlzLnN0YXRzID0gbmV3IFN0YXRzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5zdXBwcmVzc2lvbnMgPSBuZXcgU3VwcHJlc3Npb25DbGllbnQodGhpcy5yZXF1ZXN0KTtcbiAgICB0aGlzLm1lc3NhZ2VzID0gbmV3IE1lc3NhZ2VzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5yb3V0ZXMgPSBuZXcgUm91dGVzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5pcHMgPSBuZXcgSXBzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG4gICAgdGhpcy5pcF9wb29scyA9IG5ldyBJcFBvb2xzQ2xpZW50KHRoaXMucmVxdWVzdCk7XG5cbiAgICBpZiAoY29uZmlnLnB1YmxpY19rZXkpIHtcbiAgICAgIGNvbmZpZy5rZXkgPSBjb25maWcucHVibGljX2tleTtcblxuICAgICAgdGhpcy5wdWJsaWNfcmVxdWVzdCA9IG5ldyBSZXF1ZXN0KGNvbmZpZywgZm9ybURhdGEpO1xuICAgICAgdGhpcy52YWxpZGF0ZSA9IG5ldyBWYWxpZGF0ZUNsaWVudCh0aGlzLnB1YmxpY19yZXF1ZXN0KTtcbiAgICAgIHRoaXMucGFyc2UgPSBuZXcgUGFyc2VDbGllbnQodGhpcy5wdWJsaWNfcmVxdWVzdCk7XG4gICAgfVxuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5pbnRlcmZhY2UgRG9tYWluRGF0YSB7XG4gIG5hbWU6IHN0cmluZztcbiAgcmVxdWlyZV90bHM6IGFueTtcbiAgc2tpcF92ZXJpZmljYXRpb246IGFueTtcbiAgc3RhdGU6IGFueTtcbiAgd2lsZGNhcmQ6IGFueTtcbiAgc3BhbV9hY3Rpb246IGFueTtcbiAgY3JlYXRlZF9hdDogc3RyaW5nIHwgRGF0ZTtcbiAgc210cF9wYXNzd29yZDogc3RyaW5nO1xuICBzbXRwX2xvZ2luOiBzdHJpbmc7XG4gIHR5cGU6IHN0cmluZztcbn1cblxuY2xhc3MgRG9tYWluIHtcbiAgbmFtZTogYW55O1xuICByZXF1aXJlX3RsczogYW55O1xuICBza2lwX3ZlcmlmaWNhdGlvbjogYW55O1xuICBzdGF0ZTogYW55O1xuICB3aWxkY2FyZDogYW55O1xuICBzcGFtX2FjdGlvbjogYW55O1xuICBjcmVhdGVkX2F0OiBhbnk7XG4gIHNtdHBfcGFzc3dvcmQ6IGFueTtcbiAgc210cF9sb2dpbjogYW55O1xuICB0eXBlOiBhbnk7XG4gIHJlY2VpdmluZ19kbnNfcmVjb3JkczogYW55O1xuICBzZW5kaW5nX2Ruc19yZWNvcmRzOiBhbnk7XG5cbiAgY29uc3RydWN0b3IoZGF0YTogRG9tYWluRGF0YSwgcmVjZWl2aW5nPzogYW55LCBzZW5kaW5nPzogYW55KSB7XG4gICAgdGhpcy5uYW1lID0gZGF0YS5uYW1lO1xuICAgIHRoaXMucmVxdWlyZV90bHMgPSBkYXRhLnJlcXVpcmVfdGxzO1xuICAgIHRoaXMuc2tpcF92ZXJpZmljYXRpb24gPSBkYXRhLnNraXBfdmVyaWZpY2F0aW9uO1xuICAgIHRoaXMuc3RhdGUgPSBkYXRhLnN0YXRlO1xuICAgIHRoaXMud2lsZGNhcmQgPSBkYXRhLndpbGRjYXJkO1xuICAgIHRoaXMuc3BhbV9hY3Rpb24gPSBkYXRhLnNwYW1fYWN0aW9uO1xuICAgIHRoaXMuY3JlYXRlZF9hdCA9IGRhdGEuY3JlYXRlZF9hdDtcbiAgICB0aGlzLnNtdHBfcGFzc3dvcmQgPSBkYXRhLnNtdHBfcGFzc3dvcmQ7XG4gICAgdGhpcy5zbXRwX2xvZ2luID0gZGF0YS5zbXRwX2xvZ2luO1xuICAgIHRoaXMudHlwZSA9IGRhdGEudHlwZTtcblxuICAgIHRoaXMucmVjZWl2aW5nX2Ruc19yZWNvcmRzID0gcmVjZWl2aW5nIHx8IG51bGw7XG4gICAgdGhpcy5zZW5kaW5nX2Ruc19yZWNvcmRzID0gc2VuZGluZyB8fCBudWxsO1xuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIERvbWFpbkNsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VNZXNzYWdlKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBfcGFyc2VEb21haW5MaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgaXRlbXM6IERvbWFpbkRhdGFbXSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoZnVuY3Rpb24gKGl0ZW0pIHtcbiAgICAgIHJldHVybiBuZXcgRG9tYWluKGl0ZW0pO1xuICAgIH0pO1xuICB9XG5cbiAgX3BhcnNlRG9tYWluKHJlc3BvbnNlOiB7XG4gICAgYm9keToge1xuICAgICAgZG9tYWluOiBhbnksXG4gICAgICByZWNlaXZpbmdfZG5zX3JlY29yZHM6IGFueSxcbiAgICAgIHNlbmRpbmdfZG5zX3JlY29yZHM6IGFueVxuICAgIH1cbiAgfSkge1xuICAgIHJldHVybiBuZXcgRG9tYWluKFxuICAgICAgcmVzcG9uc2UuYm9keS5kb21haW4sXG4gICAgICByZXNwb25zZS5ib2R5LnJlY2VpdmluZ19kbnNfcmVjb3JkcyxcbiAgICAgIHJlc3BvbnNlLmJvZHkuc2VuZGluZ19kbnNfcmVjb3Jkc1xuICAgICk7XG4gIH1cblxuICBfcGFyc2VUcmFja2luZ1NldHRpbmdzKHJlc3BvbnNlOiB7IGJvZHk6IHsgdHJhY2tpbmc6IGFueSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS50cmFja2luZztcbiAgfVxuXG4gIF9wYXJzZVRyYWNraW5nVXBkYXRlKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSB7XG4gICAgcmV0dXJuIHJlc3BvbnNlLmJvZHk7XG4gIH1cblxuICBsaXN0KHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3YyL2RvbWFpbnMnLCBxdWVyeSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluTGlzdCk7XG4gIH1cblxuICBnZXQoZG9tYWluOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldChgL3YyL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluKTtcbiAgfVxuXG4gIGNyZWF0ZShkYXRhOiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3QoJy92Mi9kb21haW5zJywgZGF0YSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlRG9tYWluKTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YyL2RvbWFpbnMvJHtkb21haW59YClcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlTWVzc2FnZSk7XG4gIH1cblxuICAvLyBUcmFja2luZ1xuXG4gIGdldFRyYWNraW5nKGRvbWFpbjogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICd0cmFja2luZycpKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VUcmFja2luZ1NldHRpbmdzKTtcbiAgfVxuXG4gIHVwZGF0ZVRyYWNraW5nKGRvbWFpbjogc3RyaW5nLCB0eXBlOiBzdHJpbmcsIGRhdGE6IGFueSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucHV0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAndHJhY2tpbmcnLCB0eXBlKSwgZGF0YSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlVHJhY2tpbmdVcGRhdGUpO1xuICB9XG5cbiAgLy8gSVBzXG5cbiAgZ2V0SXBzKGRvbWFpbjogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBzdHJpbmdbXSB9IH0pID0+IHJlc3BvbnNlPy5ib2R5Py5pdGVtcyk7XG4gIH1cblxuICBhc3NpZ25JcChkb21haW46IHN0cmluZywgaXA6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycpLCB7IGlwIH0pO1xuICB9XG5cbiAgZGVsZXRlSXAoZG9tYWluOiBzdHJpbmcsIGlwOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZSh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycsIGlwKSk7XG4gIH1cblxuICBsaW5rSXBQb29sKGRvbWFpbjogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LnBvc3QodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICdpcHMnKSwgeyBwb29sX2lkIH0pO1xuICB9XG5cbiAgdW5saW5rSXBQb2xsKGRvbWFpbjogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcsIGlwOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZSh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ2lwcycsICdpcF9wb29sJyksIHsgcG9vbF9pZCwgaXAgfSk7XG4gIH1cbn1cbiIsImltcG9ydCBBUElFcnJvck9wdGlvbnMgZnJvbSAnLi9pbnRlcmZhY2VzL0FQSUVycm9yT3B0aW9ucyc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEFQSUVycm9yIGV4dGVuZHMgRXJyb3Ige1xuICBzdGF0dXM6IG51bWJlciB8IHN0cmluZztcbiAgc3RhY2s6IHN0cmluZztcbiAgZGV0YWlsczogc3RyaW5nO1xuXG4gIGNvbnN0cnVjdG9yKHtcbiAgICBzdGF0dXMsXG4gICAgc3RhdHVzVGV4dCxcbiAgICBtZXNzYWdlLFxuICAgIGJvZHkgPSB7fVxuICB9OiBBUElFcnJvck9wdGlvbnMpIHtcbiAgICBjb25zdCB7IG1lc3NhZ2U6IGJvZHlNZXNzYWdlLCBlcnJvciB9ID0gYm9keTtcbiAgICBzdXBlcigpO1xuXG4gICAgdGhpcy5zdGFjayA9IG51bGw7XG4gICAgdGhpcy5zdGF0dXMgPSBzdGF0dXM7XG4gICAgdGhpcy5tZXNzYWdlID0gbWVzc2FnZSB8fCBlcnJvciB8fCBzdGF0dXNUZXh0O1xuICAgIHRoaXMuZGV0YWlscyA9IGJvZHlNZXNzYWdlO1xuICB9XG59XG4iLCJjb25zdCB1cmxqb2luID0gcmVxdWlyZSgndXJsLWpvaW4nKTtcblxuY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEV2ZW50Q2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIF9wYXJzZVBhZ2VOdW1iZXIodXJsOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdXJsLnNwbGl0KCcvJykucG9wKCk7XG4gIH1cblxuICBfcGFyc2VQYWdlKGlkOiBzdHJpbmcsIHVybDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHsgaWQsIG51bWJlcjogdGhpcy5fcGFyc2VQYWdlTnVtYmVyKHVybCksIHVybCB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZUxpbmtzKHJlc3BvbnNlOiB7IGJvZHk6IHsgcGFnaW5nOiBhbnkgfSB9KSB7XG4gICAgY29uc3QgcGFnZXMgPSBPYmplY3QuZW50cmllcyhyZXNwb25zZS5ib2R5LnBhZ2luZyk7XG4gICAgcmV0dXJuIHBhZ2VzLnJlZHVjZShcbiAgICAgIChhY2M6IGFueSwgW2lkLCB1cmxdOiBbdXJsOiBzdHJpbmcsIGlkOiBzdHJpbmddKSA9PiB7XG4gICAgICAgIGFjY1tpZF0gPSB0aGlzLl9wYXJzZVBhZ2UoaWQsIHVybClcbiAgICAgICAgcmV0dXJuIGFjY1xuICAgICAgfSwge30pO1xuICB9XG5cbiAgX3BhcnNlRXZlbnRMaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgaXRlbXM6IGFueSwgcGFnaW5nOiBhbnkgfSAgfSkge1xuICAgIHJldHVybiB7XG4gICAgICBpdGVtczogcmVzcG9uc2UuYm9keS5pdGVtcyxcbiAgICAgIHBhZ2VzOiB0aGlzLl9wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSlcbiAgICB9O1xuICB9XG5cbiAgZ2V0KGRvbWFpbjogc3RyaW5nLCBxdWVyeTogeyBwYWdlOiBhbnkgfSkge1xuICAgIGxldCB1cmw7XG5cbiAgICBpZiAocXVlcnkgJiYgcXVlcnkucGFnZSkge1xuICAgICAgdXJsID0gdXJsam9pbignL3YyJywgZG9tYWluLCAnZXZlbnRzJywgcXVlcnkucGFnZSk7XG4gICAgICBkZWxldGUgcXVlcnkucGFnZTtcbiAgICB9IGVsc2Uge1xuICAgICAgdXJsID0gdXJsam9pbignL3YyJywgZG9tYWluLCAnZXZlbnRzJyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQodXJsLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSkgPT4gdGhpcy5fcGFyc2VFdmVudExpc3QocmVzcG9uc2UpKTtcbiAgfVxufVxuIiwiY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5cbmltcG9ydCB7IElwUG9vbCB9IGZyb20gXCIuL2ludGVyZmFjZXMvSXBQb29sc1wiO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBJcFBvb2xzQ2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QocXVlcnk6IGFueSk6IElwUG9vbFtdIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCgnL3YxL2lwX3Bvb2xzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogeyBpcF9wb29sczogSXBQb29sLCBtZXNzYWdlOiBzdHJpbmcgfSB9KSA9PiB0aGlzLnBhcnNlSXBQb29sc1Jlc3BvbnNlKHJlc3BvbnNlKSk7XG4gIH1cblxuICBjcmVhdGUoZGF0YTogeyBuYW1lOiBzdHJpbmcsIGRlc2NyaXB0aW9uPzogc3RyaW5nLCBpcHM/OiBzdHJpbmdbXSB9KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KCcvdjEvaXBfcG9vbHMnLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IHsgbWVzc2FnZTogc3RyaW5nLCBwb29sX2lkOiBzdHJpbmcgfSB9KSA9PiByZXNwb25zZT8uYm9keSk7XG4gIH1cblxuICB1cGRhdGUocG9vbElkOiBzdHJpbmcsIGRhdGE6IHsgbmFtZTogc3RyaW5nLCBkZXNjcmlwdGlvbjogc3RyaW5nLCBhZGRfaXA6IHN0cmluZywgcmVtb3ZlX2lwOiBzdHJpbmcgfSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QucGF0Y2goYC92MS9pcF9wb29scy8ke3Bvb2xJZH1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZT8uYm9keSk7XG4gIH1cblxuICBkZWxldGUocG9vbElkOiBzdHJpbmcsIGRhdGE6IHsgaWQ6IHN0cmluZywgcG9vbF9pZDogc3RyaW5nIH0pIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmRlbGV0ZShgL3YxL2lwX3Bvb2xzLyR7cG9vbElkfWAsIGRhdGEpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogYW55IH0pID0+IHJlc3BvbnNlPy5ib2R5KTtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VJcFBvb2xzUmVzcG9uc2UocmVzcG9uc2U6IHsgYm9keTogYW55IHwgYW55IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS5pcF9wb29scztcbiAgfVxufVxuIiwiY29uc3QgTWdSZXF1ZXN0ID0gcmVxdWlyZSgnLi9yZXF1ZXN0Jyk7XG5pbXBvcnQgeyBJcERhdGEsIElwc0xpc3RSZXNwb25zZUJvZHkgfSBmcm9tICcuL2ludGVyZmFjZXMvSXBzJztcblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgSXBzQ2xpZW50IHtcbiAgcmVxdWVzdDogdHlwZW9mIE1nUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiB0eXBlb2YgTWdSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGxpc3QocXVlcnk6IGFueSkge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KCcvdjMvaXBzJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2U6IHsgYm9keTogSXBzTGlzdFJlc3BvbnNlQm9keSB9KSA9PiB0aGlzLnBhcnNlSXBzUmVzcG9uc2UocmVzcG9uc2UpKTtcbiAgfVxuXG4gIGdldChpcDogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoYC92My9pcHMvJHtpcH1gKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IElwRGF0YSB9KSA9PiB0aGlzLnBhcnNlSXBzUmVzcG9uc2UocmVzcG9uc2UpKTtcbiAgfVxuXG4gIHByaXZhdGUgcGFyc2VJcHNSZXNwb25zZShyZXNwb25zZTogeyBib2R5OiBJcHNMaXN0UmVzcG9uc2VCb2R5IHwgSXBEYXRhIH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keTtcbiAgfVxufVxuIiwiaW1wb3J0IFJlcXVlc3QgZnJvbSBcIi4vcmVxdWVzdFwiO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBNZXNzYWdlc0NsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VSZXNwb25zZShyZXNwb25zZTogeyBib2R5OiBhbnkgfSkge1xuICAgIGlmIChyZXNwb25zZS5ib2R5KSB7XG4gICAgICByZXR1cm4gcmVzcG9uc2UuYm9keTtcbiAgICB9XG5cbiAgICByZXR1cm4gcmVzcG9uc2U7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIGRhdGE6IGFueSkge1xuICAgIGlmIChkYXRhLm1lc3NhZ2UpIHtcbiAgICAgIHJldHVybiB0aGlzLnJlcXVlc3QucG9zdE11bHRpKGAvdjMvJHtkb21haW59L21lc3NhZ2VzLm1pbWVgLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VSZXNwb25zZSk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0TXVsdGkoYC92My8ke2RvbWFpbn0vbWVzc2FnZXNgLCBkYXRhKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VSZXNwb25zZSk7XG4gIH1cbn1cbiIsImltcG9ydCBSZXF1ZXN0IGZyb20gJy4vcmVxdWVzdCc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFBhcnNlQ2xpZW50IHtcbiAgcmVxdWVzdDogUmVxdWVzdDtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIGdldChhZGRyZXNzZXM6IHN0cmluZ1tdIHwgc3RyaW5nLCBlbmFibGVEbnNFc3BDaGVja3M6IGJvb2xlYW4pIHtcbiAgICBjb25zdCBxdWVyeSA9IHt9IGFzIHsgYWRkcmVzc2VzOiBzdHJpbmcsIHN5bnRheF9vbmx5OiBib29sZWFuIH07XG5cbiAgICBpZiAoQXJyYXkuaXNBcnJheShhZGRyZXNzZXMpKSB7XG4gICAgICBhZGRyZXNzZXMgPSBhZGRyZXNzZXMuam9pbignLCcpO1xuICAgIH1cblxuICAgIHF1ZXJ5LmFkZHJlc3NlcyA9IGFkZHJlc3NlcztcblxuICAgIGlmIChlbmFibGVEbnNFc3BDaGVja3MpIHtcbiAgICAgIHF1ZXJ5LnN5bnRheF9vbmx5ID0gZmFsc2U7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9hZGRyZXNzL3BhcnNlJywgcXVlcnkpXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkpO1xuICB9XG59XG4iLCJcbmltcG9ydCBCdG9hIGZyb20gJ2J0b2EnO1xuaW1wb3J0IHVybGpvaW4gZnJvbSAndXJsLWpvaW4nO1xuaW1wb3J0IGt5IGZyb20gJ2t5LXVuaXZlcnNhbCc7XG5cbmltcG9ydCBBUElFcnJvciBmcm9tICcuL2Vycm9yJztcbmltcG9ydCBSZXF1ZXN0T3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvUmVxdWVzdE9wdGlvbnMnO1xuaW1wb3J0IEFQSUVycm9yT3B0aW9ucyBmcm9tICcuL2ludGVyZmFjZXMvQVBJRXJyb3JPcHRpb25zJztcblxuY29uc3QgaXNTdHJlYW0gPSAoYXR0YWNobWVudDogYW55KSA9PiB0eXBlb2YgYXR0YWNobWVudCA9PT0gJ29iamVjdCcgJiYgdHlwZW9mIGF0dGFjaG1lbnQucGlwZSA9PT0gJ2Z1bmN0aW9uJztcblxuY29uc3QgZ2V0QXR0YWNobWVudE9wdGlvbnMgPSAoaXRlbTogYW55KTogeyBmaWxlbmFtZT86IHN0cmluZywgY29udGVudFR5cGU/OiBzdHJpbmcsIGtub3duTGVuZ3RoPzogbnVtYmVyIH0gPT4ge1xuICBpZiAodHlwZW9mIGl0ZW0gIT09ICdvYmplY3QnIHx8IGlzU3RyZWFtKGl0ZW0pKSByZXR1cm4ge307XG5cbiAgY29uc3Qge1xuICAgIGZpbGVuYW1lLFxuICAgIGNvbnRlbnRUeXBlLFxuICAgIGtub3duTGVuZ3RoXG4gIH0gPSBpdGVtO1xuXG4gIHJldHVybiB7XG4gICAgLi4uKGZpbGVuYW1lID8geyBmaWxlbmFtZSB9IDogeyBmaWxlbmFtZTogJ2ZpbGUnIH0pLFxuICAgIC4uLihjb250ZW50VHlwZSAmJiB7IGNvbnRlbnRUeXBlIH0pLFxuICAgIC4uLihrbm93bkxlbmd0aCAmJiB7IGtub3duTGVuZ3RoIH0pXG4gIH07XG59XG5cbmNvbnN0IHN0cmVhbVRvU3RyaW5nID0gKHN0cmVhbTogYW55KSA9PiB7XG4gIGNvbnN0IGNodW5rczogYW55ID0gW11cbiAgcmV0dXJuIG5ldyBQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICBzdHJlYW0ub24oJ2RhdGEnLCAoY2h1bms6IGFueSkgPT4gY2h1bmtzLnB1c2goY2h1bmspKVxuICAgIHN0cmVhbS5vbignZXJyb3InLCByZWplY3QpXG4gICAgc3RyZWFtLm9uKCdlbmQnLCAoKSA9PiByZXNvbHZlKEJ1ZmZlci5jb25jYXQoY2h1bmtzKS50b1N0cmluZygndXRmOCcpKSlcbiAgfSlcbn1cblxuY2xhc3MgUmVxdWVzdCB7XG4gIHByaXZhdGUgdXNlcm5hbWU7XG4gIHByaXZhdGUga2V5O1xuICBwcml2YXRlIHVybDtcbiAgcHJpdmF0ZSBoZWFkZXJzOiBhbnk7XG4gIHByaXZhdGUgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YTtcblxuICBjb25zdHJ1Y3RvcihvcHRpb25zOiBSZXF1ZXN0T3B0aW9ucywgZm9ybURhdGE6IG5ldyAoKSA9PiBGb3JtRGF0YSkge1xuICAgIHRoaXMudXNlcm5hbWUgPSBvcHRpb25zLnVzZXJuYW1lO1xuICAgIHRoaXMua2V5ID0gb3B0aW9ucy5rZXk7XG4gICAgdGhpcy51cmwgPSBvcHRpb25zLnVybDtcbiAgICB0aGlzLmhlYWRlcnMgPSBvcHRpb25zLmhlYWRlcnMgfHwge307XG4gICAgdGhpcy5mb3JtRGF0YSA9IGZvcm1EYXRhO1xuICB9XG5cbiAgYXN5bmMgcmVxdWVzdChtZXRob2Q6IHN0cmluZywgdXJsOiBzdHJpbmcsIG9wdGlvbnM/OiBhbnkpIHtcbiAgICBjb25zdCBiYXNpYyA9IEJ0b2EoYCR7dGhpcy51c2VybmFtZX06JHt0aGlzLmtleX1gKTtcbiAgICBjb25zdCBoZWFkZXJzID0ge1xuICAgICAgQXV0aG9yaXphdGlvbjogYEJhc2ljICR7YmFzaWN9YCxcbiAgICAgIC4uLnRoaXMuaGVhZGVycyxcbiAgICAgIC4uLm9wdGlvbnM/LmhlYWRlcnNcbiAgICB9O1xuXG4gICAgZGVsZXRlIG9wdGlvbnM/LmhlYWRlcnM7XG5cbiAgICBpZiAoIWhlYWRlcnNbJ0NvbnRlbnQtVHlwZSddKSB7XG4gICAgICAvLyBmb3IgZm9ybS1kYXRhIGl0IHdpbGwgYmUgTnVsbCBzbyB3ZSBuZWVkIHRvIHJlbW92ZSBpdFxuICAgICAgZGVsZXRlIGhlYWRlcnNbJ0NvbnRlbnQtVHlwZSddO1xuICAgIH1cblxuICAgIGNvbnN0IHBhcmFtcyA9IHsgLi4ub3B0aW9ucyB9O1xuXG4gICAgaWYgKG9wdGlvbnM/LnF1ZXJ5ICYmIE9iamVjdC5nZXRPd25Qcm9wZXJ0eU5hbWVzKG9wdGlvbnM/LnF1ZXJ5KS5sZW5ndGggPiAwKSB7XG4gICAgICBwYXJhbXMuc2VhcmNoUGFyYW1zID0gb3B0aW9ucy5xdWVyeTtcbiAgICAgIGRlbGV0ZSBwYXJhbXMucXVlcnlcbiAgICB9XG5cbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGt5KFxuICAgICAgdXJsam9pbih0aGlzLnVybCwgdXJsKSxcbiAgICAgIHtcbiAgICAgICAgbWV0aG9kOiBtZXRob2QudG9Mb2NhbGVVcHBlckNhc2UoKSxcbiAgICAgICAgaGVhZGVycyxcbiAgICAgICAgdGhyb3dIdHRwRXJyb3JzOiBmYWxzZSxcbiAgICAgICAgLi4ucGFyYW1zXG4gICAgICB9XG4gICAgKTtcblxuICAgIGlmICghcmVzcG9uc2U/Lm9rKSB7XG4gICAgICBjb25zdCBtZXNzYWdlID0gcmVzcG9uc2U/LmJvZHkgJiYgaXNTdHJlYW0ocmVzcG9uc2UuYm9keSlcbiAgICAgICAgPyBhd2FpdCBzdHJlYW1Ub1N0cmluZyhyZXNwb25zZS5ib2R5KVxuICAgICAgICA6IGF3YWl0IHJlc3BvbnNlPy5qc29uKCk7XG5cbiAgICAgIHRocm93IG5ldyBBUElFcnJvcih7XG4gICAgICAgIHN0YXR1czogcmVzcG9uc2U/LnN0YXR1cyxcbiAgICAgICAgc3RhdHVzVGV4dDogcmVzcG9uc2U/LnN0YXR1c1RleHQsXG4gICAgICAgIGJvZHk6IHsgbWVzc2FnZSB9XG4gICAgICB9IGFzIEFQSUVycm9yT3B0aW9ucyk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIGJvZHk6IGF3YWl0IHJlc3BvbnNlPy5qc29uKCksXG4gICAgICBzdGF0dXM6IHJlc3BvbnNlPy5zdGF0dXNcbiAgICB9O1xuICB9XG5cbiAgcXVlcnkobWV0aG9kOiBzdHJpbmcsIHVybDogc3RyaW5nLCBxdWVyeTogYW55LCBvcHRpb25zPzogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdChtZXRob2QsIHVybCwgeyBxdWVyeSwgLi4ub3B0aW9ucyB9KTtcbiAgfVxuXG4gIGNvbW1hbmQobWV0aG9kOiBzdHJpbmcsIHVybDogc3RyaW5nLCBkYXRhOiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0KG1ldGhvZCwgdXJsLCB7XG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkJyB9LFxuICAgICAgYm9keTogZGF0YSxcbiAgICAgIC4uLm9wdGlvbnNcbiAgICB9KTtcbiAgfVxuXG4gIGdldCh1cmw6IHN0cmluZywgcXVlcnk/OiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5xdWVyeSgnZ2V0JywgdXJsLCBxdWVyeSwgb3B0aW9ucyk7XG4gIH1cblxuICBoZWFkKHVybDogc3RyaW5nLCBxdWVyeTogYW55LCBvcHRpb25zOiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5xdWVyeSgnaGVhZCcsIHVybCwgcXVlcnksIG9wdGlvbnMpO1xuICB9XG5cbiAgb3B0aW9ucyh1cmw6IHN0cmluZywgcXVlcnk6IGFueSwgb3B0aW9uczogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucXVlcnkoJ29wdGlvbnMnLCB1cmwsIHF1ZXJ5LCBvcHRpb25zKTtcbiAgfVxuXG4gIHBvc3QodXJsOiBzdHJpbmcsIGRhdGE6IGFueSwgb3B0aW9ucz86IGFueSkge1xuICAgIHJldHVybiB0aGlzLmNvbW1hbmQoJ3Bvc3QnLCB1cmwsIGRhdGEsIG9wdGlvbnMpO1xuICB9XG5cbiAgcG9zdE11bHRpKHVybDogc3RyaW5nLCBkYXRhOiBhbnkpIHtcblxuICAgIGNvbnN0IGZvcm1EYXRhOiBGb3JtRGF0YSA9IG5ldyB0aGlzLmZvcm1EYXRhKCk7XG4gICAgY29uc3QgcGFyYW1zOiBhbnkgPSB7XG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiBudWxsIH1cbiAgICB9O1xuXG4gICAgT2JqZWN0LmtleXMoZGF0YSlcbiAgICAgIC5maWx0ZXIoZnVuY3Rpb24gKGtleSkgeyByZXR1cm4gZGF0YVtrZXldOyB9KVxuICAgICAgLmZvckVhY2goZnVuY3Rpb24gKGtleSkge1xuICAgICAgICBpZiAoa2V5ID09PSAnYXR0YWNobWVudCcpIHtcbiAgICAgICAgICBjb25zdCBvYmogPSBkYXRhLmF0dGFjaG1lbnQ7XG5cbiAgICAgICAgICBpZiAoQXJyYXkuaXNBcnJheShvYmopKSB7XG4gICAgICAgICAgICBvYmouZm9yRWFjaChmdW5jdGlvbiAoaXRlbSkge1xuICAgICAgICAgICAgICBjb25zdCBkYXRhID0gaXRlbS5kYXRhID8gaXRlbS5kYXRhIDogaXRlbTtcbiAgICAgICAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGdldEF0dGFjaG1lbnRPcHRpb25zKGl0ZW0pO1xuICAgICAgICAgICAgICAoZm9ybURhdGEgYXMgYW55KS5hcHBlbmQoa2V5LCBkYXRhLCBvcHRpb25zKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICBjb25zdCBkYXRhID0gaXNTdHJlYW0ob2JqKSA/IG9iaiA6IG9iai5kYXRhO1xuICAgICAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGdldEF0dGFjaG1lbnRPcHRpb25zKG9iaik7XG4gICAgICAgICAgICAoZm9ybURhdGEgYXMgYW55KS5hcHBlbmQoa2V5LCBkYXRhLCBvcHRpb25zKTtcbiAgICAgICAgICB9XG5cbiAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cblxuICAgICAgICBpZiAoQXJyYXkuaXNBcnJheShkYXRhW2tleV0pKSB7XG4gICAgICAgICAgZGF0YVtrZXldLmZvckVhY2goZnVuY3Rpb24gKGl0ZW06IGFueSkge1xuICAgICAgICAgICAgZm9ybURhdGEuYXBwZW5kKGtleSwgaXRlbSk7XG4gICAgICAgICAgfSk7XG4gICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgZm9ybURhdGEuYXBwZW5kKGtleSwgZGF0YVtrZXldKTtcbiAgICAgICAgfVxuICAgICAgfSk7XG5cbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwb3N0JywgdXJsLCBmb3JtRGF0YSwgcGFyYW1zKTtcbiAgfVxuXG4gIHB1dCh1cmw6IHN0cmluZywgZGF0YTogYW55LCBvcHRpb25zPzogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMuY29tbWFuZCgncHV0JywgdXJsLCBkYXRhLCBvcHRpb25zKTtcbiAgfVxuXG4gIHBhdGNoKHVybDogc3RyaW5nLCBkYXRhOiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdwYXRjaCcsIHVybCwgZGF0YSwgb3B0aW9ucyk7XG4gIH1cblxuICBkZWxldGUodXJsOiBzdHJpbmcsIGRhdGE/OiBhbnksIG9wdGlvbnM/OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5jb21tYW5kKCdkZWxldGUnLCB1cmwsIGRhdGEsIG9wdGlvbnMpO1xuICB9XG59XG5cbmV4cG9ydCBkZWZhdWx0IFJlcXVlc3QiLCJpbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBSb3V0ZXNDbGllbnQge1xuICByZXF1ZXN0OiBSZXF1ZXN0O1xuXG4gIGNvbnN0cnVjdG9yKHJlcXVlc3Q6IFJlcXVlc3QpIHtcbiAgICB0aGlzLnJlcXVlc3QgPSByZXF1ZXN0O1xuICB9XG5cbiAgbGlzdChxdWVyeTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9yb3V0ZXMnLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5pdGVtcyk7XG4gIH1cblxuICBnZXQoaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZ2V0KGAvdjMvcm91dGVzLyR7aWR9YClcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5yb3V0ZSk7XG4gIH1cblxuICBjcmVhdGUoZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KCcvdjMvcm91dGVzJywgZGF0YSlcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keS5yb3V0ZSk7XG4gIH1cblxuICB1cGRhdGUoaWQ6IHN0cmluZywgZGF0YTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQoYC92My9yb3V0ZXMvJHtpZH1gLCBkYXRhKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlKSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxuXG4gIGRlc3Ryb3koaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKGAvdjMvcm91dGVzLyR7aWR9YClcbiAgICAgIC50aGVuKChyZXNwb25zZSkgPT4gcmVzcG9uc2UuYm9keSk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcbmltcG9ydCBSZXF1ZXN0IGZyb20gJy4vcmVxdWVzdCc7XG5pbXBvcnQgU3RhdHNPcHRpb25zIGZyb20gJy4vaW50ZXJmYWNlcy9TdGF0c09wdGlvbnMnO1xuXG5jbGFzcyBTdGF0cyB7XG4gIHN0YXJ0OiBEYXRlO1xuICBlbmQ6IERhdGU7XG4gIHJlc29sdXRpb246IHN0cmluZztcbiAgc3RhdHM6IGFueTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBTdGF0c09wdGlvbnMpIHtcbiAgICB0aGlzLnN0YXJ0ID0gbmV3IERhdGUoZGF0YS5zdGFydCk7XG4gICAgdGhpcy5lbmQgPSBuZXcgRGF0ZShkYXRhLmVuZCk7XG4gICAgdGhpcy5yZXNvbHV0aW9uID0gZGF0YS5yZXNvbHV0aW9uO1xuICAgIHRoaXMuc3RhdHMgPSBkYXRhLnN0YXRzLm1hcChmdW5jdGlvbiAoc3RhdDogeyB0aW1lOiBzdHJpbmcgfCBEYXRlIH0pIHtcbiAgICAgIHN0YXQudGltZSA9IG5ldyBEYXRlKHN0YXQudGltZSk7XG4gICAgICByZXR1cm4gc3RhdDtcbiAgICB9KTtcbiAgfVxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTdGF0c0NsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBfcGFyc2VTdGF0cyhyZXNwb25zZTogeyBib2R5OiBTdGF0c09wdGlvbnMgfSkge1xuICAgIHJldHVybiBuZXcgU3RhdHMocmVzcG9uc2UuYm9keSk7XG4gIH1cblxuICBnZXREb21haW4oZG9tYWluOiBzdHJpbmcsIHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjMnLCBkb21haW4sICdzdGF0cy90b3RhbCcpLCBxdWVyeSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlU3RhdHMpO1xuICB9XG5cbiAgZ2V0QWNjb3VudChxdWVyeTogYW55KSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9zdGF0cy90b3RhbCcsIHF1ZXJ5KVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VTdGF0cyk7XG4gIH1cbn1cbiIsImltcG9ydCB1cmwgZnJvbSAndXJsJztcbmltcG9ydCB1cmxqb2luIGZyb20gJ3VybC1qb2luJztcblxuaW1wb3J0IFJlcXVlc3QgZnJvbSAnLi9yZXF1ZXN0JztcbmltcG9ydCB7IEJvdW5jZURhdGEsIENvbXBsYWludERhdGEsIFVuc3Vic2NyaWJlRGF0YSB9IGZyb20gJy4vaW50ZXJmYWNlcy9TdXByZXNzaW9ucyc7XG5cbnR5cGUgVE1vZGVsID0gdHlwZW9mIEJvdW5jZSB8IHR5cGVvZiBDb21wbGFpbnQgfCB0eXBlb2YgVW5zdWJzY3JpYmU7XG5cbmNvbnN0IGNyZWF0ZU9wdGlvbnMgPSB7XG4gIGhlYWRlcnM6IHsgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi9qc29uJyB9XG59O1xuXG5jbGFzcyBCb3VuY2Uge1xuICB0eXBlOiBzdHJpbmc7XG4gIGFkZHJlc3M6IHN0cmluZztcbiAgY29kZTogbnVtYmVyO1xuICBlcnJvcjogc3RyaW5nO1xuICBjcmVhdGVkX2F0OiBEYXRlO1xuXG4gIGNvbnN0cnVjdG9yKGRhdGE6IEJvdW5jZURhdGEpIHtcbiAgICB0aGlzLnR5cGUgPSAnYm91bmNlcyc7XG4gICAgdGhpcy5hZGRyZXNzID0gZGF0YS5hZGRyZXNzO1xuICAgIHRoaXMuY29kZSA9ICtkYXRhLmNvZGU7XG4gICAgdGhpcy5lcnJvciA9IGRhdGEuZXJyb3I7XG4gICAgdGhpcy5jcmVhdGVkX2F0ID0gbmV3IERhdGUoZGF0YS5jcmVhdGVkX2F0KTtcbiAgfVxufVxuXG5jbGFzcyBDb21wbGFpbnQge1xuICB0eXBlOiBzdHJpbmc7XG4gIGFkZHJlc3M6IGFueTtcbiAgY3JlYXRlZF9hdDogRGF0ZTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBDb21wbGFpbnREYXRhKSB7XG4gICAgdGhpcy50eXBlID0gJ2NvbXBsYWludHMnO1xuICAgIHRoaXMuYWRkcmVzcyA9IGRhdGEuYWRkcmVzcztcbiAgICB0aGlzLmNyZWF0ZWRfYXQgPSBuZXcgRGF0ZShkYXRhLmNyZWF0ZWRfYXQpO1xuICB9XG59XG5cbmNsYXNzIFVuc3Vic2NyaWJlIHtcbiAgdHlwZTogc3RyaW5nO1xuICBhZGRyZXNzOiBzdHJpbmc7XG4gIHRhZ3M6IGFueTtcbiAgY3JlYXRlZF9hdDogRGF0ZTtcblxuICBjb25zdHJ1Y3RvcihkYXRhOiBVbnN1YnNjcmliZURhdGEpIHtcbiAgICB0aGlzLnR5cGUgPSAndW5zdWJzY3JpYmVzJztcbiAgICB0aGlzLmFkZHJlc3MgPSBkYXRhLmFkZHJlc3M7XG4gICAgdGhpcy50YWdzID0gZGF0YS50YWdzO1xuICAgIHRoaXMuY3JlYXRlZF9hdCA9IG5ldyBEYXRlKGRhdGEuY3JlYXRlZF9hdCk7XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgU3VwcHJlc3Npb25DbGllbnQge1xuICByZXF1ZXN0OiBhbnk7XG4gIG1vZGVsczoge1xuICAgIGJvdW5jZXM6IHR5cGVvZiBCb3VuY2U7XG4gICAgY29tcGxhaW50czogdHlwZW9mIENvbXBsYWludDtcbiAgICB1bnN1YnNjcmliZXM6IHR5cGVvZiBVbnN1YnNjcmliZTtcbiAgfTtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgICB0aGlzLm1vZGVscyA9IHtcbiAgICAgIGJvdW5jZXM6IEJvdW5jZSxcbiAgICAgIGNvbXBsYWludHM6IENvbXBsYWludCxcbiAgICAgIHVuc3Vic2NyaWJlczogVW5zdWJzY3JpYmVcbiAgICB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZShpZDogc3RyaW5nLCBwYWdlVXJsOiBzdHJpbmcpIHtcbiAgICBjb25zdCBwYXJzZWRVcmwgPSB1cmwucGFyc2UocGFnZVVybCwgdHJ1ZSk7XG4gICAgY29uc3QgeyBxdWVyeSB9ID0gcGFyc2VkVXJsO1xuXG4gICAgcmV0dXJuIHtcbiAgICAgIGlkLFxuICAgICAgcGFnZTogcXVlcnkucGFnZSxcbiAgICAgIGFkZHJlc3M6IHF1ZXJ5LmFkZHJlc3MsXG4gICAgICB1cmw6IHBhZ2VVcmxcbiAgICB9O1xuICB9XG5cbiAgX3BhcnNlUGFnZUxpbmtzKHJlc3BvbnNlOiB7IGJvZHk6IHsgcGFnaW5nOiBhbnkgfSB9KSB7XG4gICAgY29uc3QgcGFnZXMgPSBPYmplY3QuZW50cmllcyhyZXNwb25zZS5ib2R5LnBhZ2luZyk7XG4gICAgcmV0dXJuIHBhZ2VzLnJlZHVjZShcbiAgICAgIChhY2M6IGFueSwgW2lkLCB1cmxdOiBbdXJsOiBzdHJpbmcsIGlkOiBzdHJpbmddKSA9PiB7XG4gICAgICAgIGFjY1tpZF0gPSB0aGlzLl9wYXJzZVBhZ2UoaWQsIHVybClcbiAgICAgICAgcmV0dXJuIGFjY1xuICAgICAgfSwge30pO1xuICB9XG5cbiAgX3BhcnNlTGlzdChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSwgTW9kZWw6IFRNb2RlbCkge1xuICAgIGNvbnN0IGRhdGEgPSB7fSBhcyBhbnk7XG5cbiAgICBkYXRhLml0ZW1zID0gcmVzcG9uc2UuYm9keS5pdGVtcy5tYXAoKGQ6IGFueSkgPT4gbmV3IE1vZGVsKGQpKTtcblxuICAgIGRhdGEucGFnZXMgPSB0aGlzLl9wYXJzZVBhZ2VMaW5rcyhyZXNwb25zZSk7XG5cbiAgICByZXR1cm4gZGF0YTtcbiAgfVxuXG4gIF9wYXJzZUl0ZW0ocmVzcG9uc2U6IHsgYm9keTogYW55IH0sIE1vZGVsOiBUTW9kZWwpIHtcbiAgICByZXR1cm4gbmV3IE1vZGVsKHJlc3BvbnNlLmJvZHkpO1xuICB9XG5cbiAgbGlzdChkb21haW46IHN0cmluZywgdHlwZTogc3RyaW5nLCBxdWVyeTogYW55KSB7XG4gICAgY29uc3QgbW9kZWwgPSAodGhpcy5tb2RlbHMgYXMgYW55KVt0eXBlXTtcblxuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAgIC5nZXQodXJsam9pbigndjMnLCBkb21haW4sIHR5cGUpLCBxdWVyeSlcbiAgICAgIC50aGVuKChyZXNwb25zZTogeyBib2R5OiB7IGl0ZW1zOiBhbnksIHBhZ2luZzogYW55IH0gfSkgPT4gdGhpcy5fcGFyc2VMaXN0KHJlc3BvbnNlLCBtb2RlbCkpO1xuICB9XG5cbiAgZ2V0KGRvbWFpbjogc3RyaW5nLCB0eXBlOiBzdHJpbmcsIGFkZHJlc3M6IHN0cmluZykge1xuICAgIGNvbnN0IG1vZGVsID0gKHRoaXMubW9kZWxzIGFzIGFueSlbdHlwZV07XG5cbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0XG4gICAgICAuZ2V0KHVybGpvaW4oJ3YzJywgZG9tYWluLCB0eXBlLCBlbmNvZGVVUklDb21wb25lbnQoYWRkcmVzcykpKVxuICAgICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiB0aGlzLl9wYXJzZUl0ZW0ocmVzcG9uc2UsIG1vZGVsKSk7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIHR5cGU6IHN0cmluZywgZGF0YTogYW55KSB7XG4gICAgLy8gc3VwcG9ydHMgYWRkaW5nIG11bHRpcGxlIHN1cHByZXNzaW9ucyBieSBkZWZhdWx0XG4gICAgaWYgKCFBcnJheS5pc0FycmF5KGRhdGEpKSB7XG4gICAgICBkYXRhID0gW2RhdGFdO1xuICAgIH1cblxuICAgIHJldHVybiB0aGlzLnJlcXVlc3RcbiAgICAucG9zdCh1cmxqb2luKCd2MycsIGRvbWFpbiwgdHlwZSksIGRhdGEsIGNyZWF0ZU9wdGlvbnMpXG4gICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxuXG4gIGRlc3Ryb3koZG9tYWluOiBzdHJpbmcsIHR5cGU6IHN0cmluZywgYWRkcmVzczogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdFxuICAgIC5kZWxldGUodXJsam9pbigndjMnLCBkb21haW4sIHR5cGUsIGVuY29kZVVSSUNvbXBvbmVudChhZGRyZXNzKSkpXG4gICAgLnRoZW4oKHJlc3BvbnNlOiB7IGJvZHk6IGFueSB9KSA9PiByZXNwb25zZS5ib2R5KTtcbiAgfVxufVxuXG5tb2R1bGUuZXhwb3J0cyA9IFN1cHByZXNzaW9uQ2xpZW50O1xuIiwiXG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBWYWxpZGF0ZUNsaWVudCB7XG4gIHJlcXVlc3Q6IFJlcXVlc3Q7XG5cbiAgY29uc3RydWN0b3IocmVxdWVzdDogUmVxdWVzdCkge1xuICAgIHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG4gIH1cblxuICBnZXQoYWRkcmVzczogc3RyaW5nKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5nZXQoJy92My9hZGRyZXNzL3ZhbGlkYXRlJywgeyBhZGRyZXNzIH0pXG4gICAgICAudGhlbigocmVzcG9uc2UpID0+IHJlc3BvbnNlLmJvZHkpO1xuICB9XG59XG4iLCJpbXBvcnQgdXJsam9pbiBmcm9tICd1cmwtam9pbic7XG5pbXBvcnQgUmVxdWVzdCBmcm9tICcuL3JlcXVlc3QnO1xuXG5jbGFzcyBXZWJob29rIHtcbiAgaWQ6IHN0cmluZztcbiAgdXJsOiBzdHJpbmc7XG5cbiAgY29uc3RydWN0b3IoaWQ6IHN0cmluZywgZGF0YTogeyB1cmw6IHN0cmluZyB9KSB7XG4gICAgdGhpcy5pZCA9IGlkO1xuICAgIHRoaXMudXJsID0gZGF0YS51cmw7XG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgY2xhc3MgV2ViaG9va0NsaWVudCB7XG4gIHJlcXVlc3Q6IGFueTtcblxuICBjb25zdHJ1Y3RvcihyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gICAgdGhpcy5yZXF1ZXN0ID0gcmVxdWVzdDtcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tMaXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgd2ViaG9va3M6IGFueSB9IH0pIHtcbiAgICByZXR1cm4gcmVzcG9uc2UuYm9keS53ZWJob29rcztcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tXaXRoSUQoaWQ6IHN0cmluZykge1xuICAgIHJldHVybiBmdW5jdGlvbiAocmVzcG9uc2U6IHsgYm9keTogeyB3ZWJob29rOiBhbnkgfSB9KSB7XG4gICAgICByZXR1cm4gbmV3IFdlYmhvb2soaWQsIHJlc3BvbnNlLmJvZHkud2ViaG9vayk7XG4gICAgfTtcbiAgfVxuXG4gIF9wYXJzZVdlYmhvb2tUZXN0KHJlc3BvbnNlOiB7IGJvZHk6IHsgY29kZTogbnVtYmVyLCBtZXNzYWdlOiBzdHJpbmcgfSB9KSB7XG4gICAgcmV0dXJuIHsgY29kZTogcmVzcG9uc2UuYm9keS5jb2RlLCBtZXNzYWdlOiByZXNwb25zZS5ib2R5Lm1lc3NhZ2UgfTtcbiAgfVxuXG4gIGxpc3QoZG9tYWluOiBzdHJpbmcsIHF1ZXJ5OiBhbnkpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJyksIHF1ZXJ5KVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rTGlzdCk7XG4gIH1cblxuICBnZXQoZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcpIHtcbiAgICByZXR1cm4gdGhpcy5yZXF1ZXN0LmdldCh1cmxqb2luKCcvdjIvZG9tYWlucycsIGRvbWFpbiwgJ3dlYmhvb2tzJywgaWQpKVxuICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rV2l0aElEKGlkKSk7XG4gIH1cblxuICBjcmVhdGUoZG9tYWluOiBzdHJpbmcsIGlkOiBzdHJpbmcsIHVybDogc3RyaW5nLCB0ZXN0OiBib29sZWFuKSB7XG4gICAgaWYgKHRlc3QpIHtcbiAgICAgIHJldHVybiB0aGlzLnJlcXVlc3QucHV0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnLCBpZCwgJ3Rlc3QnKSwgeyB1cmwgfSlcbiAgICAgICAgLnRoZW4odGhpcy5fcGFyc2VXZWJob29rVGVzdCk7XG4gICAgfVxuXG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wb3N0KHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnKSwgeyBpZCwgdXJsIH0pXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tXaXRoSUQoaWQpKTtcbiAgfVxuXG4gIHVwZGF0ZShkb21haW46IHN0cmluZywgaWQ6IHN0cmluZywgdXJsOiBzdHJpbmcsKSB7XG4gICAgcmV0dXJuIHRoaXMucmVxdWVzdC5wdXQodXJsam9pbignL3YyL2RvbWFpbnMnLCBkb21haW4sICd3ZWJob29rcycsIGlkKSwgeyB1cmwgfSlcbiAgICAgIC50aGVuKHRoaXMuX3BhcnNlV2ViaG9va1dpdGhJRChpZCkpO1xuICB9XG5cbiAgZGVzdHJveShkb21haW46IHN0cmluZywgaWQ6IHN0cmluZykge1xuICAgIHJldHVybiB0aGlzLnJlcXVlc3QuZGVsZXRlKHVybGpvaW4oJy92Mi9kb21haW5zJywgZG9tYWluLCAnd2ViaG9va3MnLCBpZCkpXG4gICAgICAudGhlbih0aGlzLl9wYXJzZVdlYmhvb2tXaXRoSUQoaWQpKTtcbiAgfVxufVxuIiwiKGZ1bmN0aW9uICgpIHtcbiAgXCJ1c2Ugc3RyaWN0XCI7XG5cbiAgZnVuY3Rpb24gYnRvYShzdHIpIHtcbiAgICB2YXIgYnVmZmVyO1xuXG4gICAgaWYgKHN0ciBpbnN0YW5jZW9mIEJ1ZmZlcikge1xuICAgICAgYnVmZmVyID0gc3RyO1xuICAgIH0gZWxzZSB7XG4gICAgICBidWZmZXIgPSBCdWZmZXIuZnJvbShzdHIudG9TdHJpbmcoKSwgJ2JpbmFyeScpO1xuICAgIH1cblxuICAgIHJldHVybiBidWZmZXIudG9TdHJpbmcoJ2Jhc2U2NCcpO1xuICB9XG5cbiAgbW9kdWxlLmV4cG9ydHMgPSBidG9hO1xufSgpKTtcbiIsIlwidXNlIHN0cmljdFwiO1xuLyoqXG4gKiBSZXR1cm5zIGEgYEJ1ZmZlcmAgaW5zdGFuY2UgZnJvbSB0aGUgZ2l2ZW4gZGF0YSBVUkkgYHVyaWAuXG4gKlxuICogQHBhcmFtIHtTdHJpbmd9IHVyaSBEYXRhIFVSSSB0byB0dXJuIGludG8gYSBCdWZmZXIgaW5zdGFuY2VcbiAqIEByZXR1cm4ge0J1ZmZlcn0gQnVmZmVyIGluc3RhbmNlIGZyb20gRGF0YSBVUklcbiAqIEBhcGkgcHVibGljXG4gKi9cbmZ1bmN0aW9uIGRhdGFVcmlUb0J1ZmZlcih1cmkpIHtcbiAgICBpZiAoIS9eZGF0YTovaS50ZXN0KHVyaSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignYHVyaWAgZG9lcyBub3QgYXBwZWFyIHRvIGJlIGEgRGF0YSBVUkkgKG11c3QgYmVnaW4gd2l0aCBcImRhdGE6XCIpJyk7XG4gICAgfVxuICAgIC8vIHN0cmlwIG5ld2xpbmVzXG4gICAgdXJpID0gdXJpLnJlcGxhY2UoL1xccj9cXG4vZywgJycpO1xuICAgIC8vIHNwbGl0IHRoZSBVUkkgdXAgaW50byB0aGUgXCJtZXRhZGF0YVwiIGFuZCB0aGUgXCJkYXRhXCIgcG9ydGlvbnNcbiAgICBjb25zdCBmaXJzdENvbW1hID0gdXJpLmluZGV4T2YoJywnKTtcbiAgICBpZiAoZmlyc3RDb21tYSA9PT0gLTEgfHwgZmlyc3RDb21tYSA8PSA0KSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ21hbGZvcm1lZCBkYXRhOiBVUkknKTtcbiAgICB9XG4gICAgLy8gcmVtb3ZlIHRoZSBcImRhdGE6XCIgc2NoZW1lIGFuZCBwYXJzZSB0aGUgbWV0YWRhdGFcbiAgICBjb25zdCBtZXRhID0gdXJpLnN1YnN0cmluZyg1LCBmaXJzdENvbW1hKS5zcGxpdCgnOycpO1xuICAgIGxldCBjaGFyc2V0ID0gJyc7XG4gICAgbGV0IGJhc2U2NCA9IGZhbHNlO1xuICAgIGNvbnN0IHR5cGUgPSBtZXRhWzBdIHx8ICd0ZXh0L3BsYWluJztcbiAgICBsZXQgdHlwZUZ1bGwgPSB0eXBlO1xuICAgIGZvciAobGV0IGkgPSAxOyBpIDwgbWV0YS5sZW5ndGg7IGkrKykge1xuICAgICAgICBpZiAobWV0YVtpXSA9PT0gJ2Jhc2U2NCcpIHtcbiAgICAgICAgICAgIGJhc2U2NCA9IHRydWU7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICB0eXBlRnVsbCArPSBgOyR7bWV0YVtpXX1gO1xuICAgICAgICAgICAgaWYgKG1ldGFbaV0uaW5kZXhPZignY2hhcnNldD0nKSA9PT0gMCkge1xuICAgICAgICAgICAgICAgIGNoYXJzZXQgPSBtZXRhW2ldLnN1YnN0cmluZyg4KTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBkZWZhdWx0cyB0byBVUy1BU0NJSSBvbmx5IGlmIHR5cGUgaXMgbm90IHByb3ZpZGVkXG4gICAgaWYgKCFtZXRhWzBdICYmICFjaGFyc2V0Lmxlbmd0aCkge1xuICAgICAgICB0eXBlRnVsbCArPSAnO2NoYXJzZXQ9VVMtQVNDSUknO1xuICAgICAgICBjaGFyc2V0ID0gJ1VTLUFTQ0lJJztcbiAgICB9XG4gICAgLy8gZ2V0IHRoZSBlbmNvZGVkIGRhdGEgcG9ydGlvbiBhbmQgZGVjb2RlIFVSSS1lbmNvZGVkIGNoYXJzXG4gICAgY29uc3QgZW5jb2RpbmcgPSBiYXNlNjQgPyAnYmFzZTY0JyA6ICdhc2NpaSc7XG4gICAgY29uc3QgZGF0YSA9IHVuZXNjYXBlKHVyaS5zdWJzdHJpbmcoZmlyc3RDb21tYSArIDEpKTtcbiAgICBjb25zdCBidWZmZXIgPSBCdWZmZXIuZnJvbShkYXRhLCBlbmNvZGluZyk7XG4gICAgLy8gc2V0IGAudHlwZWAgYW5kIGAudHlwZUZ1bGxgIHByb3BlcnRpZXMgdG8gTUlNRSB0eXBlXG4gICAgYnVmZmVyLnR5cGUgPSB0eXBlO1xuICAgIGJ1ZmZlci50eXBlRnVsbCA9IHR5cGVGdWxsO1xuICAgIC8vIHNldCB0aGUgYC5jaGFyc2V0YCBwcm9wZXJ0eVxuICAgIGJ1ZmZlci5jaGFyc2V0ID0gY2hhcnNldDtcbiAgICByZXR1cm4gYnVmZmVyO1xufVxubW9kdWxlLmV4cG9ydHMgPSBkYXRhVXJpVG9CdWZmZXI7XG4vLyMgc291cmNlTWFwcGluZ1VSTD1pbmRleC5qcy5tYXAiLCIvKipcbiAqIEBhdXRob3IgVG9ydSBOYWdhc2hpbWEgPGh0dHBzOi8vZ2l0aHViLmNvbS9teXN0aWNhdGVhPlxuICogQGNvcHlyaWdodCAyMDE1IFRvcnUgTmFnYXNoaW1hLiBBbGwgcmlnaHRzIHJlc2VydmVkLlxuICogU2VlIExJQ0VOU0UgZmlsZSBpbiByb290IGRpcmVjdG9yeSBmb3IgZnVsbCBsaWNlbnNlLlxuICovXG4ndXNlIHN0cmljdCc7XG5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHsgdmFsdWU6IHRydWUgfSk7XG5cbi8qKlxuICogQHR5cGVkZWYge29iamVjdH0gUHJpdmF0ZURhdGFcbiAqIEBwcm9wZXJ0eSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQuXG4gKiBAcHJvcGVydHkge3t0eXBlOnN0cmluZ319IGV2ZW50IFRoZSBvcmlnaW5hbCBldmVudCBvYmplY3QuXG4gKiBAcHJvcGVydHkge251bWJlcn0gZXZlbnRQaGFzZSBUaGUgY3VycmVudCBldmVudCBwaGFzZS5cbiAqIEBwcm9wZXJ0eSB7RXZlbnRUYXJnZXR8bnVsbH0gY3VycmVudFRhcmdldCBUaGUgY3VycmVudCBldmVudCB0YXJnZXQuXG4gKiBAcHJvcGVydHkge2Jvb2xlYW59IGNhbmNlbGVkIFRoZSBmbGFnIHRvIHByZXZlbnQgZGVmYXVsdC5cbiAqIEBwcm9wZXJ0eSB7Ym9vbGVhbn0gc3RvcHBlZCBUaGUgZmxhZyB0byBzdG9wIHByb3BhZ2F0aW9uLlxuICogQHByb3BlcnR5IHtib29sZWFufSBpbW1lZGlhdGVTdG9wcGVkIFRoZSBmbGFnIHRvIHN0b3AgcHJvcGFnYXRpb24gaW1tZWRpYXRlbHkuXG4gKiBAcHJvcGVydHkge0Z1bmN0aW9ufG51bGx9IHBhc3NpdmVMaXN0ZW5lciBUaGUgbGlzdGVuZXIgaWYgdGhlIGN1cnJlbnQgbGlzdGVuZXIgaXMgcGFzc2l2ZS4gT3RoZXJ3aXNlIHRoaXMgaXMgbnVsbC5cbiAqIEBwcm9wZXJ0eSB7bnVtYmVyfSB0aW1lU3RhbXAgVGhlIHVuaXggdGltZS5cbiAqIEBwcml2YXRlXG4gKi9cblxuLyoqXG4gKiBQcml2YXRlIGRhdGEgZm9yIGV2ZW50IHdyYXBwZXJzLlxuICogQHR5cGUge1dlYWtNYXA8RXZlbnQsIFByaXZhdGVEYXRhPn1cbiAqIEBwcml2YXRlXG4gKi9cbmNvbnN0IHByaXZhdGVEYXRhID0gbmV3IFdlYWtNYXAoKTtcblxuLyoqXG4gKiBDYWNoZSBmb3Igd3JhcHBlciBjbGFzc2VzLlxuICogQHR5cGUge1dlYWtNYXA8T2JqZWN0LCBGdW5jdGlvbj59XG4gKiBAcHJpdmF0ZVxuICovXG5jb25zdCB3cmFwcGVycyA9IG5ldyBXZWFrTWFwKCk7XG5cbi8qKlxuICogR2V0IHByaXZhdGUgZGF0YS5cbiAqIEBwYXJhbSB7RXZlbnR9IGV2ZW50IFRoZSBldmVudCBvYmplY3QgdG8gZ2V0IHByaXZhdGUgZGF0YS5cbiAqIEByZXR1cm5zIHtQcml2YXRlRGF0YX0gVGhlIHByaXZhdGUgZGF0YSBvZiB0aGUgZXZlbnQuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBwZChldmVudCkge1xuICAgIGNvbnN0IHJldHYgPSBwcml2YXRlRGF0YS5nZXQoZXZlbnQpO1xuICAgIGNvbnNvbGUuYXNzZXJ0KFxuICAgICAgICByZXR2ICE9IG51bGwsXG4gICAgICAgIFwiJ3RoaXMnIGlzIGV4cGVjdGVkIGFuIEV2ZW50IG9iamVjdCwgYnV0IGdvdFwiLFxuICAgICAgICBldmVudFxuICAgICk7XG4gICAgcmV0dXJuIHJldHZcbn1cblxuLyoqXG4gKiBodHRwczovL2RvbS5zcGVjLndoYXR3Zy5vcmcvI3NldC10aGUtY2FuY2VsZWQtZmxhZ1xuICogQHBhcmFtIGRhdGEge1ByaXZhdGVEYXRhfSBwcml2YXRlIGRhdGEuXG4gKi9cbmZ1bmN0aW9uIHNldENhbmNlbEZsYWcoZGF0YSkge1xuICAgIGlmIChkYXRhLnBhc3NpdmVMaXN0ZW5lciAhPSBudWxsKSB7XG4gICAgICAgIGlmIChcbiAgICAgICAgICAgIHR5cGVvZiBjb25zb2xlICE9PSBcInVuZGVmaW5lZFwiICYmXG4gICAgICAgICAgICB0eXBlb2YgY29uc29sZS5lcnJvciA9PT0gXCJmdW5jdGlvblwiXG4gICAgICAgICkge1xuICAgICAgICAgICAgY29uc29sZS5lcnJvcihcbiAgICAgICAgICAgICAgICBcIlVuYWJsZSB0byBwcmV2ZW50RGVmYXVsdCBpbnNpZGUgcGFzc2l2ZSBldmVudCBsaXN0ZW5lciBpbnZvY2F0aW9uLlwiLFxuICAgICAgICAgICAgICAgIGRhdGEucGFzc2l2ZUxpc3RlbmVyXG4gICAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgICAgIHJldHVyblxuICAgIH1cbiAgICBpZiAoIWRhdGEuZXZlbnQuY2FuY2VsYWJsZSkge1xuICAgICAgICByZXR1cm5cbiAgICB9XG5cbiAgICBkYXRhLmNhbmNlbGVkID0gdHJ1ZTtcbiAgICBpZiAodHlwZW9mIGRhdGEuZXZlbnQucHJldmVudERlZmF1bHQgPT09IFwiZnVuY3Rpb25cIikge1xuICAgICAgICBkYXRhLmV2ZW50LnByZXZlbnREZWZhdWx0KCk7XG4gICAgfVxufVxuXG4vKipcbiAqIEBzZWUgaHR0cHM6Ly9kb20uc3BlYy53aGF0d2cub3JnLyNpbnRlcmZhY2UtZXZlbnRcbiAqIEBwcml2YXRlXG4gKi9cbi8qKlxuICogVGhlIGV2ZW50IHdyYXBwZXIuXG4gKiBAY29uc3RydWN0b3JcbiAqIEBwYXJhbSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQgb2YgdGhpcyBkaXNwYXRjaGluZy5cbiAqIEBwYXJhbSB7RXZlbnR8e3R5cGU6c3RyaW5nfX0gZXZlbnQgVGhlIG9yaWdpbmFsIGV2ZW50IHRvIHdyYXAuXG4gKi9cbmZ1bmN0aW9uIEV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgIHByaXZhdGVEYXRhLnNldCh0aGlzLCB7XG4gICAgICAgIGV2ZW50VGFyZ2V0LFxuICAgICAgICBldmVudCxcbiAgICAgICAgZXZlbnRQaGFzZTogMixcbiAgICAgICAgY3VycmVudFRhcmdldDogZXZlbnRUYXJnZXQsXG4gICAgICAgIGNhbmNlbGVkOiBmYWxzZSxcbiAgICAgICAgc3RvcHBlZDogZmFsc2UsXG4gICAgICAgIGltbWVkaWF0ZVN0b3BwZWQ6IGZhbHNlLFxuICAgICAgICBwYXNzaXZlTGlzdGVuZXI6IG51bGwsXG4gICAgICAgIHRpbWVTdGFtcDogZXZlbnQudGltZVN0YW1wIHx8IERhdGUubm93KCksXG4gICAgfSk7XG5cbiAgICAvLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNVbmZvcmdlYWJsZVxuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0aGlzLCBcImlzVHJ1c3RlZFwiLCB7IHZhbHVlOiBmYWxzZSwgZW51bWVyYWJsZTogdHJ1ZSB9KTtcblxuICAgIC8vIERlZmluZSBhY2Nlc3NvcnNcbiAgICBjb25zdCBrZXlzID0gT2JqZWN0LmtleXMoZXZlbnQpO1xuICAgIGZvciAobGV0IGkgPSAwOyBpIDwga2V5cy5sZW5ndGg7ICsraSkge1xuICAgICAgICBjb25zdCBrZXkgPSBrZXlzW2ldO1xuICAgICAgICBpZiAoIShrZXkgaW4gdGhpcykpIHtcbiAgICAgICAgICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eSh0aGlzLCBrZXksIGRlZmluZVJlZGlyZWN0RGVzY3JpcHRvcihrZXkpKTtcbiAgICAgICAgfVxuICAgIH1cbn1cblxuLy8gU2hvdWxkIGJlIGVudW1lcmFibGUsIGJ1dCBjbGFzcyBtZXRob2RzIGFyZSBub3QgZW51bWVyYWJsZS5cbkV2ZW50LnByb3RvdHlwZSA9IHtcbiAgICAvKipcbiAgICAgKiBUaGUgdHlwZSBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtzdHJpbmd9XG4gICAgICovXG4gICAgZ2V0IHR5cGUoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5ldmVudC50eXBlXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB0YXJnZXQgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7RXZlbnRUYXJnZXR9XG4gICAgICovXG4gICAgZ2V0IHRhcmdldCgpIHtcbiAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmV2ZW50VGFyZ2V0XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB0YXJnZXQgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7RXZlbnRUYXJnZXR9XG4gICAgICovXG4gICAgZ2V0IGN1cnJlbnRUYXJnZXQoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5jdXJyZW50VGFyZ2V0XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIEByZXR1cm5zIHtFdmVudFRhcmdldFtdfSBUaGUgY29tcG9zZWQgcGF0aCBvZiB0aGlzIGV2ZW50LlxuICAgICAqL1xuICAgIGNvbXBvc2VkUGF0aCgpIHtcbiAgICAgICAgY29uc3QgY3VycmVudFRhcmdldCA9IHBkKHRoaXMpLmN1cnJlbnRUYXJnZXQ7XG4gICAgICAgIGlmIChjdXJyZW50VGFyZ2V0ID09IG51bGwpIHtcbiAgICAgICAgICAgIHJldHVybiBbXVxuICAgICAgICB9XG4gICAgICAgIHJldHVybiBbY3VycmVudFRhcmdldF1cbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogQ29uc3RhbnQgb2YgTk9ORS5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCBOT05FKCkge1xuICAgICAgICByZXR1cm4gMFxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBDQVBUVVJJTkdfUEhBU0UuXG4gICAgICogQHR5cGUge251bWJlcn1cbiAgICAgKi9cbiAgICBnZXQgQ0FQVFVSSU5HX1BIQVNFKCkge1xuICAgICAgICByZXR1cm4gMVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBBVF9UQVJHRVQuXG4gICAgICogQHR5cGUge251bWJlcn1cbiAgICAgKi9cbiAgICBnZXQgQVRfVEFSR0VUKCkge1xuICAgICAgICByZXR1cm4gMlxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDb25zdGFudCBvZiBCVUJCTElOR19QSEFTRS5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCBCVUJCTElOR19QSEFTRSgpIHtcbiAgICAgICAgcmV0dXJuIDNcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIHRhcmdldCBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtudW1iZXJ9XG4gICAgICovXG4gICAgZ2V0IGV2ZW50UGhhc2UoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5ldmVudFBoYXNlXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFN0b3AgZXZlbnQgYnViYmxpbmcuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgc3RvcFByb3BhZ2F0aW9uKCkge1xuICAgICAgICBjb25zdCBkYXRhID0gcGQodGhpcyk7XG5cbiAgICAgICAgZGF0YS5zdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgaWYgKHR5cGVvZiBkYXRhLmV2ZW50LnN0b3BQcm9wYWdhdGlvbiA9PT0gXCJmdW5jdGlvblwiKSB7XG4gICAgICAgICAgICBkYXRhLmV2ZW50LnN0b3BQcm9wYWdhdGlvbigpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFN0b3AgZXZlbnQgYnViYmxpbmcuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgc3RvcEltbWVkaWF0ZVByb3BhZ2F0aW9uKCkge1xuICAgICAgICBjb25zdCBkYXRhID0gcGQodGhpcyk7XG5cbiAgICAgICAgZGF0YS5zdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgZGF0YS5pbW1lZGlhdGVTdG9wcGVkID0gdHJ1ZTtcbiAgICAgICAgaWYgKHR5cGVvZiBkYXRhLmV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbiA9PT0gXCJmdW5jdGlvblwiKSB7XG4gICAgICAgICAgICBkYXRhLmV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbigpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGJlIGJ1YmJsaW5nLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBidWJibGVzKCkge1xuICAgICAgICByZXR1cm4gQm9vbGVhbihwZCh0aGlzKS5ldmVudC5idWJibGVzKVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBUaGUgZmxhZyB0byBiZSBjYW5jZWxhYmxlLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBjYW5jZWxhYmxlKCkge1xuICAgICAgICByZXR1cm4gQm9vbGVhbihwZCh0aGlzKS5ldmVudC5jYW5jZWxhYmxlKVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBDYW5jZWwgdGhpcyBldmVudC5cbiAgICAgKiBAcmV0dXJucyB7dm9pZH1cbiAgICAgKi9cbiAgICBwcmV2ZW50RGVmYXVsdCgpIHtcbiAgICAgICAgc2V0Q2FuY2VsRmxhZyhwZCh0aGlzKSk7XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGluZGljYXRlIGNhbmNlbGxhdGlvbiBzdGF0ZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKi9cbiAgICBnZXQgZGVmYXVsdFByZXZlbnRlZCgpIHtcbiAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmNhbmNlbGVkXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGJlIGNvbXBvc2VkLlxuICAgICAqIEB0eXBlIHtib29sZWFufVxuICAgICAqL1xuICAgIGdldCBjb21wb3NlZCgpIHtcbiAgICAgICAgcmV0dXJuIEJvb2xlYW4ocGQodGhpcykuZXZlbnQuY29tcG9zZWQpXG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSB1bml4IHRpbWUgb2YgdGhpcyBldmVudC5cbiAgICAgKiBAdHlwZSB7bnVtYmVyfVxuICAgICAqL1xuICAgIGdldCB0aW1lU3RhbXAoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS50aW1lU3RhbXBcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIHRhcmdldCBvZiB0aGlzIGV2ZW50LlxuICAgICAqIEB0eXBlIHtFdmVudFRhcmdldH1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCBzcmNFbGVtZW50KCkge1xuICAgICAgICByZXR1cm4gcGQodGhpcykuZXZlbnRUYXJnZXRcbiAgICB9LFxuXG4gICAgLyoqXG4gICAgICogVGhlIGZsYWcgdG8gc3RvcCBldmVudCBidWJibGluZy5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCBjYW5jZWxCdWJibGUoKSB7XG4gICAgICAgIHJldHVybiBwZCh0aGlzKS5zdG9wcGVkXG4gICAgfSxcbiAgICBzZXQgY2FuY2VsQnViYmxlKHZhbHVlKSB7XG4gICAgICAgIGlmICghdmFsdWUpIHtcbiAgICAgICAgICAgIHJldHVyblxuICAgICAgICB9XG4gICAgICAgIGNvbnN0IGRhdGEgPSBwZCh0aGlzKTtcblxuICAgICAgICBkYXRhLnN0b3BwZWQgPSB0cnVlO1xuICAgICAgICBpZiAodHlwZW9mIGRhdGEuZXZlbnQuY2FuY2VsQnViYmxlID09PSBcImJvb2xlYW5cIikge1xuICAgICAgICAgICAgZGF0YS5ldmVudC5jYW5jZWxCdWJibGUgPSB0cnVlO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFRoZSBmbGFnIHRvIGluZGljYXRlIGNhbmNlbGxhdGlvbiBzdGF0ZS5cbiAgICAgKiBAdHlwZSB7Ym9vbGVhbn1cbiAgICAgKiBAZGVwcmVjYXRlZFxuICAgICAqL1xuICAgIGdldCByZXR1cm5WYWx1ZSgpIHtcbiAgICAgICAgcmV0dXJuICFwZCh0aGlzKS5jYW5jZWxlZFxuICAgIH0sXG4gICAgc2V0IHJldHVyblZhbHVlKHZhbHVlKSB7XG4gICAgICAgIGlmICghdmFsdWUpIHtcbiAgICAgICAgICAgIHNldENhbmNlbEZsYWcocGQodGhpcykpO1xuICAgICAgICB9XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIEluaXRpYWxpemUgdGhpcyBldmVudCBvYmplY3QuIEJ1dCBkbyBub3RoaW5nIHVuZGVyIGV2ZW50IGRpc3BhdGNoaW5nLlxuICAgICAqIEBwYXJhbSB7c3RyaW5nfSB0eXBlIFRoZSBldmVudCB0eXBlLlxuICAgICAqIEBwYXJhbSB7Ym9vbGVhbn0gW2J1YmJsZXM9ZmFsc2VdIFRoZSBmbGFnIHRvIGJlIHBvc3NpYmxlIHRvIGJ1YmJsZSB1cC5cbiAgICAgKiBAcGFyYW0ge2Jvb2xlYW59IFtjYW5jZWxhYmxlPWZhbHNlXSBUaGUgZmxhZyB0byBiZSBwb3NzaWJsZSB0byBjYW5jZWwuXG4gICAgICogQGRlcHJlY2F0ZWRcbiAgICAgKi9cbiAgICBpbml0RXZlbnQoKSB7XG4gICAgICAgIC8vIERvIG5vdGhpbmcuXG4gICAgfSxcbn07XG5cbi8vIGBjb25zdHJ1Y3RvcmAgaXMgbm90IGVudW1lcmFibGUuXG5PYmplY3QuZGVmaW5lUHJvcGVydHkoRXZlbnQucHJvdG90eXBlLCBcImNvbnN0cnVjdG9yXCIsIHtcbiAgICB2YWx1ZTogRXZlbnQsXG4gICAgY29uZmlndXJhYmxlOiB0cnVlLFxuICAgIHdyaXRhYmxlOiB0cnVlLFxufSk7XG5cbi8vIEVuc3VyZSBgZXZlbnQgaW5zdGFuY2VvZiB3aW5kb3cuRXZlbnRgIGlzIGB0cnVlYC5cbmlmICh0eXBlb2Ygd2luZG93ICE9PSBcInVuZGVmaW5lZFwiICYmIHR5cGVvZiB3aW5kb3cuRXZlbnQgIT09IFwidW5kZWZpbmVkXCIpIHtcbiAgICBPYmplY3Quc2V0UHJvdG90eXBlT2YoRXZlbnQucHJvdG90eXBlLCB3aW5kb3cuRXZlbnQucHJvdG90eXBlKTtcblxuICAgIC8vIE1ha2UgYXNzb2NpYXRpb24gZm9yIHdyYXBwZXJzLlxuICAgIHdyYXBwZXJzLnNldCh3aW5kb3cuRXZlbnQucHJvdG90eXBlLCBFdmVudCk7XG59XG5cbi8qKlxuICogR2V0IHRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yIHRvIHJlZGlyZWN0IGEgZ2l2ZW4gcHJvcGVydHkuXG4gKiBAcGFyYW0ge3N0cmluZ30ga2V5IFByb3BlcnR5IG5hbWUgdG8gZGVmaW5lIHByb3BlcnR5IGRlc2NyaXB0b3IuXG4gKiBAcmV0dXJucyB7UHJvcGVydHlEZXNjcmlwdG9yfSBUaGUgcHJvcGVydHkgZGVzY3JpcHRvciB0byByZWRpcmVjdCB0aGUgcHJvcGVydHkuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBkZWZpbmVSZWRpcmVjdERlc2NyaXB0b3Ioa2V5KSB7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgZ2V0KCkge1xuICAgICAgICAgICAgcmV0dXJuIHBkKHRoaXMpLmV2ZW50W2tleV1cbiAgICAgICAgfSxcbiAgICAgICAgc2V0KHZhbHVlKSB7XG4gICAgICAgICAgICBwZCh0aGlzKS5ldmVudFtrZXldID0gdmFsdWU7XG4gICAgICAgIH0sXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZSxcbiAgICAgICAgZW51bWVyYWJsZTogdHJ1ZSxcbiAgICB9XG59XG5cbi8qKlxuICogR2V0IHRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yIHRvIGNhbGwgYSBnaXZlbiBtZXRob2QgcHJvcGVydHkuXG4gKiBAcGFyYW0ge3N0cmluZ30ga2V5IFByb3BlcnR5IG5hbWUgdG8gZGVmaW5lIHByb3BlcnR5IGRlc2NyaXB0b3IuXG4gKiBAcmV0dXJucyB7UHJvcGVydHlEZXNjcmlwdG9yfSBUaGUgcHJvcGVydHkgZGVzY3JpcHRvciB0byBjYWxsIHRoZSBtZXRob2QgcHJvcGVydHkuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBkZWZpbmVDYWxsRGVzY3JpcHRvcihrZXkpIHtcbiAgICByZXR1cm4ge1xuICAgICAgICB2YWx1ZSgpIHtcbiAgICAgICAgICAgIGNvbnN0IGV2ZW50ID0gcGQodGhpcykuZXZlbnQ7XG4gICAgICAgICAgICByZXR1cm4gZXZlbnRba2V5XS5hcHBseShldmVudCwgYXJndW1lbnRzKVxuICAgICAgICB9LFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgfVxufVxuXG4vKipcbiAqIERlZmluZSBuZXcgd3JhcHBlciBjbGFzcy5cbiAqIEBwYXJhbSB7RnVuY3Rpb259IEJhc2VFdmVudCBUaGUgYmFzZSB3cmFwcGVyIGNsYXNzLlxuICogQHBhcmFtIHtPYmplY3R9IHByb3RvIFRoZSBwcm90b3R5cGUgb2YgdGhlIG9yaWdpbmFsIGV2ZW50LlxuICogQHJldHVybnMge0Z1bmN0aW9ufSBUaGUgZGVmaW5lZCB3cmFwcGVyIGNsYXNzLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lV3JhcHBlcihCYXNlRXZlbnQsIHByb3RvKSB7XG4gICAgY29uc3Qga2V5cyA9IE9iamVjdC5rZXlzKHByb3RvKTtcbiAgICBpZiAoa2V5cy5sZW5ndGggPT09IDApIHtcbiAgICAgICAgcmV0dXJuIEJhc2VFdmVudFxuICAgIH1cblxuICAgIC8qKiBDdXN0b21FdmVudCAqL1xuICAgIGZ1bmN0aW9uIEN1c3RvbUV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgICAgICBCYXNlRXZlbnQuY2FsbCh0aGlzLCBldmVudFRhcmdldCwgZXZlbnQpO1xuICAgIH1cblxuICAgIEN1c3RvbUV2ZW50LnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoQmFzZUV2ZW50LnByb3RvdHlwZSwge1xuICAgICAgICBjb25zdHJ1Y3RvcjogeyB2YWx1ZTogQ3VzdG9tRXZlbnQsIGNvbmZpZ3VyYWJsZTogdHJ1ZSwgd3JpdGFibGU6IHRydWUgfSxcbiAgICB9KTtcblxuICAgIC8vIERlZmluZSBhY2Nlc3NvcnMuXG4gICAgZm9yIChsZXQgaSA9IDA7IGkgPCBrZXlzLmxlbmd0aDsgKytpKSB7XG4gICAgICAgIGNvbnN0IGtleSA9IGtleXNbaV07XG4gICAgICAgIGlmICghKGtleSBpbiBCYXNlRXZlbnQucHJvdG90eXBlKSkge1xuICAgICAgICAgICAgY29uc3QgZGVzY3JpcHRvciA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IocHJvdG8sIGtleSk7XG4gICAgICAgICAgICBjb25zdCBpc0Z1bmMgPSB0eXBlb2YgZGVzY3JpcHRvci52YWx1ZSA9PT0gXCJmdW5jdGlvblwiO1xuICAgICAgICAgICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFxuICAgICAgICAgICAgICAgIEN1c3RvbUV2ZW50LnByb3RvdHlwZSxcbiAgICAgICAgICAgICAgICBrZXksXG4gICAgICAgICAgICAgICAgaXNGdW5jXG4gICAgICAgICAgICAgICAgICAgID8gZGVmaW5lQ2FsbERlc2NyaXB0b3Ioa2V5KVxuICAgICAgICAgICAgICAgICAgICA6IGRlZmluZVJlZGlyZWN0RGVzY3JpcHRvcihrZXkpXG4gICAgICAgICAgICApO1xuICAgICAgICB9XG4gICAgfVxuXG4gICAgcmV0dXJuIEN1c3RvbUV2ZW50XG59XG5cbi8qKlxuICogR2V0IHRoZSB3cmFwcGVyIGNsYXNzIG9mIGEgZ2l2ZW4gcHJvdG90eXBlLlxuICogQHBhcmFtIHtPYmplY3R9IHByb3RvIFRoZSBwcm90b3R5cGUgb2YgdGhlIG9yaWdpbmFsIGV2ZW50IHRvIGdldCBpdHMgd3JhcHBlci5cbiAqIEByZXR1cm5zIHtGdW5jdGlvbn0gVGhlIHdyYXBwZXIgY2xhc3MuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBnZXRXcmFwcGVyKHByb3RvKSB7XG4gICAgaWYgKHByb3RvID09IG51bGwgfHwgcHJvdG8gPT09IE9iamVjdC5wcm90b3R5cGUpIHtcbiAgICAgICAgcmV0dXJuIEV2ZW50XG4gICAgfVxuXG4gICAgbGV0IHdyYXBwZXIgPSB3cmFwcGVycy5nZXQocHJvdG8pO1xuICAgIGlmICh3cmFwcGVyID09IG51bGwpIHtcbiAgICAgICAgd3JhcHBlciA9IGRlZmluZVdyYXBwZXIoZ2V0V3JhcHBlcihPYmplY3QuZ2V0UHJvdG90eXBlT2YocHJvdG8pKSwgcHJvdG8pO1xuICAgICAgICB3cmFwcGVycy5zZXQocHJvdG8sIHdyYXBwZXIpO1xuICAgIH1cbiAgICByZXR1cm4gd3JhcHBlclxufVxuXG4vKipcbiAqIFdyYXAgYSBnaXZlbiBldmVudCB0byBtYW5hZ2VtZW50IGEgZGlzcGF0Y2hpbmcuXG4gKiBAcGFyYW0ge0V2ZW50VGFyZ2V0fSBldmVudFRhcmdldCBUaGUgZXZlbnQgdGFyZ2V0IG9mIHRoaXMgZGlzcGF0Y2hpbmcuXG4gKiBAcGFyYW0ge09iamVjdH0gZXZlbnQgVGhlIGV2ZW50IHRvIHdyYXAuXG4gKiBAcmV0dXJucyB7RXZlbnR9IFRoZSB3cmFwcGVyIGluc3RhbmNlLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gd3JhcEV2ZW50KGV2ZW50VGFyZ2V0LCBldmVudCkge1xuICAgIGNvbnN0IFdyYXBwZXIgPSBnZXRXcmFwcGVyKE9iamVjdC5nZXRQcm90b3R5cGVPZihldmVudCkpO1xuICAgIHJldHVybiBuZXcgV3JhcHBlcihldmVudFRhcmdldCwgZXZlbnQpXG59XG5cbi8qKlxuICogR2V0IHRoZSBpbW1lZGlhdGVTdG9wcGVkIGZsYWcgb2YgYSBnaXZlbiBldmVudC5cbiAqIEBwYXJhbSB7RXZlbnR9IGV2ZW50IFRoZSBldmVudCB0byBnZXQuXG4gKiBAcmV0dXJucyB7Ym9vbGVhbn0gVGhlIGZsYWcgdG8gc3RvcCBwcm9wYWdhdGlvbiBpbW1lZGlhdGVseS5cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIGlzU3RvcHBlZChldmVudCkge1xuICAgIHJldHVybiBwZChldmVudCkuaW1tZWRpYXRlU3RvcHBlZFxufVxuXG4vKipcbiAqIFNldCB0aGUgY3VycmVudCBldmVudCBwaGFzZSBvZiBhIGdpdmVuIGV2ZW50LlxuICogQHBhcmFtIHtFdmVudH0gZXZlbnQgVGhlIGV2ZW50IHRvIHNldCBjdXJyZW50IHRhcmdldC5cbiAqIEBwYXJhbSB7bnVtYmVyfSBldmVudFBoYXNlIE5ldyBldmVudCBwaGFzZS5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gc2V0RXZlbnRQaGFzZShldmVudCwgZXZlbnRQaGFzZSkge1xuICAgIHBkKGV2ZW50KS5ldmVudFBoYXNlID0gZXZlbnRQaGFzZTtcbn1cblxuLyoqXG4gKiBTZXQgdGhlIGN1cnJlbnQgdGFyZ2V0IG9mIGEgZ2l2ZW4gZXZlbnQuXG4gKiBAcGFyYW0ge0V2ZW50fSBldmVudCBUaGUgZXZlbnQgdG8gc2V0IGN1cnJlbnQgdGFyZ2V0LlxuICogQHBhcmFtIHtFdmVudFRhcmdldHxudWxsfSBjdXJyZW50VGFyZ2V0IE5ldyBjdXJyZW50IHRhcmdldC5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gc2V0Q3VycmVudFRhcmdldChldmVudCwgY3VycmVudFRhcmdldCkge1xuICAgIHBkKGV2ZW50KS5jdXJyZW50VGFyZ2V0ID0gY3VycmVudFRhcmdldDtcbn1cblxuLyoqXG4gKiBTZXQgYSBwYXNzaXZlIGxpc3RlbmVyIG9mIGEgZ2l2ZW4gZXZlbnQuXG4gKiBAcGFyYW0ge0V2ZW50fSBldmVudCBUaGUgZXZlbnQgdG8gc2V0IGN1cnJlbnQgdGFyZ2V0LlxuICogQHBhcmFtIHtGdW5jdGlvbnxudWxsfSBwYXNzaXZlTGlzdGVuZXIgTmV3IHBhc3NpdmUgbGlzdGVuZXIuXG4gKiBAcmV0dXJucyB7dm9pZH1cbiAqIEBwcml2YXRlXG4gKi9cbmZ1bmN0aW9uIHNldFBhc3NpdmVMaXN0ZW5lcihldmVudCwgcGFzc2l2ZUxpc3RlbmVyKSB7XG4gICAgcGQoZXZlbnQpLnBhc3NpdmVMaXN0ZW5lciA9IHBhc3NpdmVMaXN0ZW5lcjtcbn1cblxuLyoqXG4gKiBAdHlwZWRlZiB7b2JqZWN0fSBMaXN0ZW5lck5vZGVcbiAqIEBwcm9wZXJ0eSB7RnVuY3Rpb259IGxpc3RlbmVyXG4gKiBAcHJvcGVydHkgezF8MnwzfSBsaXN0ZW5lclR5cGVcbiAqIEBwcm9wZXJ0eSB7Ym9vbGVhbn0gcGFzc2l2ZVxuICogQHByb3BlcnR5IHtib29sZWFufSBvbmNlXG4gKiBAcHJvcGVydHkge0xpc3RlbmVyTm9kZXxudWxsfSBuZXh0XG4gKiBAcHJpdmF0ZVxuICovXG5cbi8qKlxuICogQHR5cGUge1dlYWtNYXA8b2JqZWN0LCBNYXA8c3RyaW5nLCBMaXN0ZW5lck5vZGU+Pn1cbiAqIEBwcml2YXRlXG4gKi9cbmNvbnN0IGxpc3RlbmVyc01hcCA9IG5ldyBXZWFrTWFwKCk7XG5cbi8vIExpc3RlbmVyIHR5cGVzXG5jb25zdCBDQVBUVVJFID0gMTtcbmNvbnN0IEJVQkJMRSA9IDI7XG5jb25zdCBBVFRSSUJVVEUgPSAzO1xuXG4vKipcbiAqIENoZWNrIHdoZXRoZXIgYSBnaXZlbiB2YWx1ZSBpcyBhbiBvYmplY3Qgb3Igbm90LlxuICogQHBhcmFtIHthbnl9IHggVGhlIHZhbHVlIHRvIGNoZWNrLlxuICogQHJldHVybnMge2Jvb2xlYW59IGB0cnVlYCBpZiB0aGUgdmFsdWUgaXMgYW4gb2JqZWN0LlxuICovXG5mdW5jdGlvbiBpc09iamVjdCh4KSB7XG4gICAgcmV0dXJuIHggIT09IG51bGwgJiYgdHlwZW9mIHggPT09IFwib2JqZWN0XCIgLy9lc2xpbnQtZGlzYWJsZS1saW5lIG5vLXJlc3RyaWN0ZWQtc3ludGF4XG59XG5cbi8qKlxuICogR2V0IGxpc3RlbmVycy5cbiAqIEBwYXJhbSB7RXZlbnRUYXJnZXR9IGV2ZW50VGFyZ2V0IFRoZSBldmVudCB0YXJnZXQgdG8gZ2V0LlxuICogQHJldHVybnMge01hcDxzdHJpbmcsIExpc3RlbmVyTm9kZT59IFRoZSBsaXN0ZW5lcnMuXG4gKiBAcHJpdmF0ZVxuICovXG5mdW5jdGlvbiBnZXRMaXN0ZW5lcnMoZXZlbnRUYXJnZXQpIHtcbiAgICBjb25zdCBsaXN0ZW5lcnMgPSBsaXN0ZW5lcnNNYXAuZ2V0KGV2ZW50VGFyZ2V0KTtcbiAgICBpZiAobGlzdGVuZXJzID09IG51bGwpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcbiAgICAgICAgICAgIFwiJ3RoaXMnIGlzIGV4cGVjdGVkIGFuIEV2ZW50VGFyZ2V0IG9iamVjdCwgYnV0IGdvdCBhbm90aGVyIHZhbHVlLlwiXG4gICAgICAgIClcbiAgICB9XG4gICAgcmV0dXJuIGxpc3RlbmVyc1xufVxuXG4vKipcbiAqIEdldCB0aGUgcHJvcGVydHkgZGVzY3JpcHRvciBmb3IgdGhlIGV2ZW50IGF0dHJpYnV0ZSBvZiBhIGdpdmVuIGV2ZW50LlxuICogQHBhcmFtIHtzdHJpbmd9IGV2ZW50TmFtZSBUaGUgZXZlbnQgbmFtZSB0byBnZXQgcHJvcGVydHkgZGVzY3JpcHRvci5cbiAqIEByZXR1cm5zIHtQcm9wZXJ0eURlc2NyaXB0b3J9IFRoZSBwcm9wZXJ0eSBkZXNjcmlwdG9yLlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lRXZlbnRBdHRyaWJ1dGVEZXNjcmlwdG9yKGV2ZW50TmFtZSkge1xuICAgIHJldHVybiB7XG4gICAgICAgIGdldCgpIHtcbiAgICAgICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcbiAgICAgICAgICAgIGxldCBub2RlID0gbGlzdGVuZXJzLmdldChldmVudE5hbWUpO1xuICAgICAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgICAgIGlmIChub2RlLmxpc3RlbmVyVHlwZSA9PT0gQVRUUklCVVRFKSB7XG4gICAgICAgICAgICAgICAgICAgIHJldHVybiBub2RlLmxpc3RlbmVyXG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICByZXR1cm4gbnVsbFxuICAgICAgICB9LFxuXG4gICAgICAgIHNldChsaXN0ZW5lcikge1xuICAgICAgICAgICAgaWYgKHR5cGVvZiBsaXN0ZW5lciAhPT0gXCJmdW5jdGlvblwiICYmICFpc09iamVjdChsaXN0ZW5lcikpIHtcbiAgICAgICAgICAgICAgICBsaXN0ZW5lciA9IG51bGw7IC8vIGVzbGludC1kaXNhYmxlLWxpbmUgbm8tcGFyYW0tcmVhc3NpZ25cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcblxuICAgICAgICAgICAgLy8gVHJhdmVyc2UgdG8gdGhlIHRhaWwgd2hpbGUgcmVtb3Zpbmcgb2xkIHZhbHVlLlxuICAgICAgICAgICAgbGV0IHByZXYgPSBudWxsO1xuICAgICAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgICAgICB3aGlsZSAobm9kZSAhPSBudWxsKSB7XG4gICAgICAgICAgICAgICAgaWYgKG5vZGUubGlzdGVuZXJUeXBlID09PSBBVFRSSUJVVEUpIHtcbiAgICAgICAgICAgICAgICAgICAgLy8gUmVtb3ZlIG9sZCB2YWx1ZS5cbiAgICAgICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIHByZXYubmV4dCA9IG5vZGUubmV4dDtcbiAgICAgICAgICAgICAgICAgICAgfSBlbHNlIGlmIChub2RlLm5leHQgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgbGlzdGVuZXJzLmRlbGV0ZShldmVudE5hbWUpO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldiA9IG5vZGU7XG4gICAgICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAgICAgbm9kZSA9IG5vZGUubmV4dDtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQWRkIG5ldyB2YWx1ZS5cbiAgICAgICAgICAgIGlmIChsaXN0ZW5lciAhPT0gbnVsbCkge1xuICAgICAgICAgICAgICAgIGNvbnN0IG5ld05vZGUgPSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVyLFxuICAgICAgICAgICAgICAgICAgICBsaXN0ZW5lclR5cGU6IEFUVFJJQlVURSxcbiAgICAgICAgICAgICAgICAgICAgcGFzc2l2ZTogZmFsc2UsXG4gICAgICAgICAgICAgICAgICAgIG9uY2U6IGZhbHNlLFxuICAgICAgICAgICAgICAgICAgICBuZXh0OiBudWxsLFxuICAgICAgICAgICAgICAgIH07XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgPT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgbGlzdGVuZXJzLnNldChldmVudE5hbWUsIG5ld05vZGUpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIHByZXYubmV4dCA9IG5ld05vZGU7XG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgfVxuICAgICAgICB9LFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgIGVudW1lcmFibGU6IHRydWUsXG4gICAgfVxufVxuXG4vKipcbiAqIERlZmluZSBhbiBldmVudCBhdHRyaWJ1dGUgKGUuZy4gYGV2ZW50VGFyZ2V0Lm9uY2xpY2tgKS5cbiAqIEBwYXJhbSB7T2JqZWN0fSBldmVudFRhcmdldFByb3RvdHlwZSBUaGUgZXZlbnQgdGFyZ2V0IHByb3RvdHlwZSB0byBkZWZpbmUgYW4gZXZlbnQgYXR0cmJpdGUuXG4gKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lIFRoZSBldmVudCBuYW1lIHRvIGRlZmluZS5cbiAqIEByZXR1cm5zIHt2b2lkfVxuICovXG5mdW5jdGlvbiBkZWZpbmVFdmVudEF0dHJpYnV0ZShldmVudFRhcmdldFByb3RvdHlwZSwgZXZlbnROYW1lKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFxuICAgICAgICBldmVudFRhcmdldFByb3RvdHlwZSxcbiAgICAgICAgYG9uJHtldmVudE5hbWV9YCxcbiAgICAgICAgZGVmaW5lRXZlbnRBdHRyaWJ1dGVEZXNjcmlwdG9yKGV2ZW50TmFtZSlcbiAgICApO1xufVxuXG4vKipcbiAqIERlZmluZSBhIGN1c3RvbSBFdmVudFRhcmdldCB3aXRoIGV2ZW50IGF0dHJpYnV0ZXMuXG4gKiBAcGFyYW0ge3N0cmluZ1tdfSBldmVudE5hbWVzIEV2ZW50IG5hbWVzIGZvciBldmVudCBhdHRyaWJ1dGVzLlxuICogQHJldHVybnMge0V2ZW50VGFyZ2V0fSBUaGUgY3VzdG9tIEV2ZW50VGFyZ2V0LlxuICogQHByaXZhdGVcbiAqL1xuZnVuY3Rpb24gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQoZXZlbnROYW1lcykge1xuICAgIC8qKiBDdXN0b21FdmVudFRhcmdldCAqL1xuICAgIGZ1bmN0aW9uIEN1c3RvbUV2ZW50VGFyZ2V0KCkge1xuICAgICAgICBFdmVudFRhcmdldC5jYWxsKHRoaXMpO1xuICAgIH1cblxuICAgIEN1c3RvbUV2ZW50VGFyZ2V0LnByb3RvdHlwZSA9IE9iamVjdC5jcmVhdGUoRXZlbnRUYXJnZXQucHJvdG90eXBlLCB7XG4gICAgICAgIGNvbnN0cnVjdG9yOiB7XG4gICAgICAgICAgICB2YWx1ZTogQ3VzdG9tRXZlbnRUYXJnZXQsXG4gICAgICAgICAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgICAgICAgICB3cml0YWJsZTogdHJ1ZSxcbiAgICAgICAgfSxcbiAgICB9KTtcblxuICAgIGZvciAobGV0IGkgPSAwOyBpIDwgZXZlbnROYW1lcy5sZW5ndGg7ICsraSkge1xuICAgICAgICBkZWZpbmVFdmVudEF0dHJpYnV0ZShDdXN0b21FdmVudFRhcmdldC5wcm90b3R5cGUsIGV2ZW50TmFtZXNbaV0pO1xuICAgIH1cblxuICAgIHJldHVybiBDdXN0b21FdmVudFRhcmdldFxufVxuXG4vKipcbiAqIEV2ZW50VGFyZ2V0LlxuICpcbiAqIC0gVGhpcyBpcyBjb25zdHJ1Y3RvciBpZiBubyBhcmd1bWVudHMuXG4gKiAtIFRoaXMgaXMgYSBmdW5jdGlvbiB3aGljaCByZXR1cm5zIGEgQ3VzdG9tRXZlbnRUYXJnZXQgY29uc3RydWN0b3IgaWYgdGhlcmUgYXJlIGFyZ3VtZW50cy5cbiAqXG4gKiBGb3IgZXhhbXBsZTpcbiAqXG4gKiAgICAgY2xhc3MgQSBleHRlbmRzIEV2ZW50VGFyZ2V0IHt9XG4gKiAgICAgY2xhc3MgQiBleHRlbmRzIEV2ZW50VGFyZ2V0KFwibWVzc2FnZVwiKSB7fVxuICogICAgIGNsYXNzIEMgZXh0ZW5kcyBFdmVudFRhcmdldChcIm1lc3NhZ2VcIiwgXCJlcnJvclwiKSB7fVxuICogICAgIGNsYXNzIEQgZXh0ZW5kcyBFdmVudFRhcmdldChbXCJtZXNzYWdlXCIsIFwiZXJyb3JcIl0pIHt9XG4gKi9cbmZ1bmN0aW9uIEV2ZW50VGFyZ2V0KCkge1xuICAgIC8qZXNsaW50LWRpc2FibGUgY29uc2lzdGVudC1yZXR1cm4gKi9cbiAgICBpZiAodGhpcyBpbnN0YW5jZW9mIEV2ZW50VGFyZ2V0KSB7XG4gICAgICAgIGxpc3RlbmVyc01hcC5zZXQodGhpcywgbmV3IE1hcCgpKTtcbiAgICAgICAgcmV0dXJuXG4gICAgfVxuICAgIGlmIChhcmd1bWVudHMubGVuZ3RoID09PSAxICYmIEFycmF5LmlzQXJyYXkoYXJndW1lbnRzWzBdKSkge1xuICAgICAgICByZXR1cm4gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQoYXJndW1lbnRzWzBdKVxuICAgIH1cbiAgICBpZiAoYXJndW1lbnRzLmxlbmd0aCA+IDApIHtcbiAgICAgICAgY29uc3QgdHlwZXMgPSBuZXcgQXJyYXkoYXJndW1lbnRzLmxlbmd0aCk7XG4gICAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgYXJndW1lbnRzLmxlbmd0aDsgKytpKSB7XG4gICAgICAgICAgICB0eXBlc1tpXSA9IGFyZ3VtZW50c1tpXTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gZGVmaW5lQ3VzdG9tRXZlbnRUYXJnZXQodHlwZXMpXG4gICAgfVxuICAgIHRocm93IG5ldyBUeXBlRXJyb3IoXCJDYW5ub3QgY2FsbCBhIGNsYXNzIGFzIGEgZnVuY3Rpb25cIilcbiAgICAvKmVzbGludC1lbmFibGUgY29uc2lzdGVudC1yZXR1cm4gKi9cbn1cblxuLy8gU2hvdWxkIGJlIGVudW1lcmFibGUsIGJ1dCBjbGFzcyBtZXRob2RzIGFyZSBub3QgZW51bWVyYWJsZS5cbkV2ZW50VGFyZ2V0LnByb3RvdHlwZSA9IHtcbiAgICAvKipcbiAgICAgKiBBZGQgYSBnaXZlbiBsaXN0ZW5lciB0byB0aGlzIGV2ZW50IHRhcmdldC5cbiAgICAgKiBAcGFyYW0ge3N0cmluZ30gZXZlbnROYW1lIFRoZSBldmVudCBuYW1lIHRvIGFkZC5cbiAgICAgKiBAcGFyYW0ge0Z1bmN0aW9ufSBsaXN0ZW5lciBUaGUgbGlzdGVuZXIgdG8gYWRkLlxuICAgICAqIEBwYXJhbSB7Ym9vbGVhbnx7Y2FwdHVyZT86Ym9vbGVhbixwYXNzaXZlPzpib29sZWFuLG9uY2U/OmJvb2xlYW59fSBbb3B0aW9uc10gVGhlIG9wdGlvbnMgZm9yIHRoaXMgbGlzdGVuZXIuXG4gICAgICogQHJldHVybnMge3ZvaWR9XG4gICAgICovXG4gICAgYWRkRXZlbnRMaXN0ZW5lcihldmVudE5hbWUsIGxpc3RlbmVyLCBvcHRpb25zKSB7XG4gICAgICAgIGlmIChsaXN0ZW5lciA9PSBudWxsKSB7XG4gICAgICAgICAgICByZXR1cm5cbiAgICAgICAgfVxuICAgICAgICBpZiAodHlwZW9mIGxpc3RlbmVyICE9PSBcImZ1bmN0aW9uXCIgJiYgIWlzT2JqZWN0KGxpc3RlbmVyKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihcIidsaXN0ZW5lcicgc2hvdWxkIGJlIGEgZnVuY3Rpb24gb3IgYW4gb2JqZWN0LlwiKVxuICAgICAgICB9XG5cbiAgICAgICAgY29uc3QgbGlzdGVuZXJzID0gZ2V0TGlzdGVuZXJzKHRoaXMpO1xuICAgICAgICBjb25zdCBvcHRpb25zSXNPYmogPSBpc09iamVjdChvcHRpb25zKTtcbiAgICAgICAgY29uc3QgY2FwdHVyZSA9IG9wdGlvbnNJc09ialxuICAgICAgICAgICAgPyBCb29sZWFuKG9wdGlvbnMuY2FwdHVyZSlcbiAgICAgICAgICAgIDogQm9vbGVhbihvcHRpb25zKTtcbiAgICAgICAgY29uc3QgbGlzdGVuZXJUeXBlID0gY2FwdHVyZSA/IENBUFRVUkUgOiBCVUJCTEU7XG4gICAgICAgIGNvbnN0IG5ld05vZGUgPSB7XG4gICAgICAgICAgICBsaXN0ZW5lcixcbiAgICAgICAgICAgIGxpc3RlbmVyVHlwZSxcbiAgICAgICAgICAgIHBhc3NpdmU6IG9wdGlvbnNJc09iaiAmJiBCb29sZWFuKG9wdGlvbnMucGFzc2l2ZSksXG4gICAgICAgICAgICBvbmNlOiBvcHRpb25zSXNPYmogJiYgQm9vbGVhbihvcHRpb25zLm9uY2UpLFxuICAgICAgICAgICAgbmV4dDogbnVsbCxcbiAgICAgICAgfTtcblxuICAgICAgICAvLyBTZXQgaXQgYXMgdGhlIGZpcnN0IG5vZGUgaWYgdGhlIGZpcnN0IG5vZGUgaXMgbnVsbC5cbiAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgIGlmIChub2RlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBuZXdOb2RlKTtcbiAgICAgICAgICAgIHJldHVyblxuICAgICAgICB9XG5cbiAgICAgICAgLy8gVHJhdmVyc2UgdG8gdGhlIHRhaWwgd2hpbGUgY2hlY2tpbmcgZHVwbGljYXRpb24uLlxuICAgICAgICBsZXQgcHJldiA9IG51bGw7XG4gICAgICAgIHdoaWxlIChub2RlICE9IG51bGwpIHtcbiAgICAgICAgICAgIGlmIChcbiAgICAgICAgICAgICAgICBub2RlLmxpc3RlbmVyID09PSBsaXN0ZW5lciAmJlxuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXJUeXBlID09PSBsaXN0ZW5lclR5cGVcbiAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICAgIC8vIFNob3VsZCBpZ25vcmUgZHVwbGljYXRpb24uXG4gICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBwcmV2ID0gbm9kZTtcbiAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBBZGQgaXQuXG4gICAgICAgIHByZXYubmV4dCA9IG5ld05vZGU7XG4gICAgfSxcblxuICAgIC8qKlxuICAgICAqIFJlbW92ZSBhIGdpdmVuIGxpc3RlbmVyIGZyb20gdGhpcyBldmVudCB0YXJnZXQuXG4gICAgICogQHBhcmFtIHtzdHJpbmd9IGV2ZW50TmFtZSBUaGUgZXZlbnQgbmFtZSB0byByZW1vdmUuXG4gICAgICogQHBhcmFtIHtGdW5jdGlvbn0gbGlzdGVuZXIgVGhlIGxpc3RlbmVyIHRvIHJlbW92ZS5cbiAgICAgKiBAcGFyYW0ge2Jvb2xlYW58e2NhcHR1cmU/OmJvb2xlYW4scGFzc2l2ZT86Ym9vbGVhbixvbmNlPzpib29sZWFufX0gW29wdGlvbnNdIFRoZSBvcHRpb25zIGZvciB0aGlzIGxpc3RlbmVyLlxuICAgICAqIEByZXR1cm5zIHt2b2lkfVxuICAgICAqL1xuICAgIHJlbW92ZUV2ZW50TGlzdGVuZXIoZXZlbnROYW1lLCBsaXN0ZW5lciwgb3B0aW9ucykge1xuICAgICAgICBpZiAobGlzdGVuZXIgPT0gbnVsbCkge1xuICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cblxuICAgICAgICBjb25zdCBsaXN0ZW5lcnMgPSBnZXRMaXN0ZW5lcnModGhpcyk7XG4gICAgICAgIGNvbnN0IGNhcHR1cmUgPSBpc09iamVjdChvcHRpb25zKVxuICAgICAgICAgICAgPyBCb29sZWFuKG9wdGlvbnMuY2FwdHVyZSlcbiAgICAgICAgICAgIDogQm9vbGVhbihvcHRpb25zKTtcbiAgICAgICAgY29uc3QgbGlzdGVuZXJUeXBlID0gY2FwdHVyZSA/IENBUFRVUkUgOiBCVUJCTEU7XG5cbiAgICAgICAgbGV0IHByZXYgPSBudWxsO1xuICAgICAgICBsZXQgbm9kZSA9IGxpc3RlbmVycy5nZXQoZXZlbnROYW1lKTtcbiAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgaWYgKFxuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIgPT09IGxpc3RlbmVyICYmXG4gICAgICAgICAgICAgICAgbm9kZS5saXN0ZW5lclR5cGUgPT09IGxpc3RlbmVyVHlwZVxuICAgICAgICAgICAgKSB7XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldi5uZXh0ID0gbm9kZS5uZXh0O1xuICAgICAgICAgICAgICAgIH0gZWxzZSBpZiAobm9kZS5uZXh0ICE9PSBudWxsKSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5kZWxldGUoZXZlbnROYW1lKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIHByZXYgPSBub2RlO1xuICAgICAgICAgICAgbm9kZSA9IG5vZGUubmV4dDtcbiAgICAgICAgfVxuICAgIH0sXG5cbiAgICAvKipcbiAgICAgKiBEaXNwYXRjaCBhIGdpdmVuIGV2ZW50LlxuICAgICAqIEBwYXJhbSB7RXZlbnR8e3R5cGU6c3RyaW5nfX0gZXZlbnQgVGhlIGV2ZW50IHRvIGRpc3BhdGNoLlxuICAgICAqIEByZXR1cm5zIHtib29sZWFufSBgZmFsc2VgIGlmIGNhbmNlbGVkLlxuICAgICAqL1xuICAgIGRpc3BhdGNoRXZlbnQoZXZlbnQpIHtcbiAgICAgICAgaWYgKGV2ZW50ID09IG51bGwgfHwgdHlwZW9mIGV2ZW50LnR5cGUgIT09IFwic3RyaW5nXCIpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1wiZXZlbnQudHlwZVwiIHNob3VsZCBiZSBhIHN0cmluZy4nKVxuICAgICAgICB9XG5cbiAgICAgICAgLy8gSWYgbGlzdGVuZXJzIGFyZW4ndCByZWdpc3RlcmVkLCB0ZXJtaW5hdGUuXG4gICAgICAgIGNvbnN0IGxpc3RlbmVycyA9IGdldExpc3RlbmVycyh0aGlzKTtcbiAgICAgICAgY29uc3QgZXZlbnROYW1lID0gZXZlbnQudHlwZTtcbiAgICAgICAgbGV0IG5vZGUgPSBsaXN0ZW5lcnMuZ2V0KGV2ZW50TmFtZSk7XG4gICAgICAgIGlmIChub2RlID09IG51bGwpIHtcbiAgICAgICAgICAgIHJldHVybiB0cnVlXG4gICAgICAgIH1cblxuICAgICAgICAvLyBTaW5jZSB3ZSBjYW5ub3QgcmV3cml0ZSBzZXZlcmFsIHByb3BlcnRpZXMsIHNvIHdyYXAgb2JqZWN0LlxuICAgICAgICBjb25zdCB3cmFwcGVkRXZlbnQgPSB3cmFwRXZlbnQodGhpcywgZXZlbnQpO1xuXG4gICAgICAgIC8vIFRoaXMgZG9lc24ndCBwcm9jZXNzIGNhcHR1cmluZyBwaGFzZSBhbmQgYnViYmxpbmcgcGhhc2UuXG4gICAgICAgIC8vIFRoaXMgaXNuJ3QgcGFydGljaXBhdGluZyBpbiBhIHRyZWUuXG4gICAgICAgIGxldCBwcmV2ID0gbnVsbDtcbiAgICAgICAgd2hpbGUgKG5vZGUgIT0gbnVsbCkge1xuICAgICAgICAgICAgLy8gUmVtb3ZlIHRoaXMgbGlzdGVuZXIgaWYgaXQncyBvbmNlXG4gICAgICAgICAgICBpZiAobm9kZS5vbmNlKSB7XG4gICAgICAgICAgICAgICAgaWYgKHByZXYgIT09IG51bGwpIHtcbiAgICAgICAgICAgICAgICAgICAgcHJldi5uZXh0ID0gbm9kZS5uZXh0O1xuICAgICAgICAgICAgICAgIH0gZWxzZSBpZiAobm9kZS5uZXh0ICE9PSBudWxsKSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5zZXQoZXZlbnROYW1lLCBub2RlLm5leHQpO1xuICAgICAgICAgICAgICAgIH0gZWxzZSB7XG4gICAgICAgICAgICAgICAgICAgIGxpc3RlbmVycy5kZWxldGUoZXZlbnROYW1lKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgICAgIHByZXYgPSBub2RlO1xuICAgICAgICAgICAgfVxuXG4gICAgICAgICAgICAvLyBDYWxsIHRoaXMgbGlzdGVuZXJcbiAgICAgICAgICAgIHNldFBhc3NpdmVMaXN0ZW5lcihcbiAgICAgICAgICAgICAgICB3cmFwcGVkRXZlbnQsXG4gICAgICAgICAgICAgICAgbm9kZS5wYXNzaXZlID8gbm9kZS5saXN0ZW5lciA6IG51bGxcbiAgICAgICAgICAgICk7XG4gICAgICAgICAgICBpZiAodHlwZW9mIG5vZGUubGlzdGVuZXIgPT09IFwiZnVuY3Rpb25cIikge1xuICAgICAgICAgICAgICAgIHRyeSB7XG4gICAgICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIuY2FsbCh0aGlzLCB3cmFwcGVkRXZlbnQpO1xuICAgICAgICAgICAgICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgICAgICAgICAgICAgICBpZiAoXG4gICAgICAgICAgICAgICAgICAgICAgICB0eXBlb2YgY29uc29sZSAhPT0gXCJ1bmRlZmluZWRcIiAmJlxuICAgICAgICAgICAgICAgICAgICAgICAgdHlwZW9mIGNvbnNvbGUuZXJyb3IgPT09IFwiZnVuY3Rpb25cIlxuICAgICAgICAgICAgICAgICAgICApIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUuZXJyb3IoZXJyKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH0gZWxzZSBpZiAoXG4gICAgICAgICAgICAgICAgbm9kZS5saXN0ZW5lclR5cGUgIT09IEFUVFJJQlVURSAmJlxuICAgICAgICAgICAgICAgIHR5cGVvZiBub2RlLmxpc3RlbmVyLmhhbmRsZUV2ZW50ID09PSBcImZ1bmN0aW9uXCJcbiAgICAgICAgICAgICkge1xuICAgICAgICAgICAgICAgIG5vZGUubGlzdGVuZXIuaGFuZGxlRXZlbnQod3JhcHBlZEV2ZW50KTtcbiAgICAgICAgICAgIH1cblxuICAgICAgICAgICAgLy8gQnJlYWsgaWYgYGV2ZW50LnN0b3BJbW1lZGlhdGVQcm9wYWdhdGlvbmAgd2FzIGNhbGxlZC5cbiAgICAgICAgICAgIGlmIChpc1N0b3BwZWQod3JhcHBlZEV2ZW50KSkge1xuICAgICAgICAgICAgICAgIGJyZWFrXG4gICAgICAgICAgICB9XG5cbiAgICAgICAgICAgIG5vZGUgPSBub2RlLm5leHQ7XG4gICAgICAgIH1cbiAgICAgICAgc2V0UGFzc2l2ZUxpc3RlbmVyKHdyYXBwZWRFdmVudCwgbnVsbCk7XG4gICAgICAgIHNldEV2ZW50UGhhc2Uod3JhcHBlZEV2ZW50LCAwKTtcbiAgICAgICAgc2V0Q3VycmVudFRhcmdldCh3cmFwcGVkRXZlbnQsIG51bGwpO1xuXG4gICAgICAgIHJldHVybiAhd3JhcHBlZEV2ZW50LmRlZmF1bHRQcmV2ZW50ZWRcbiAgICB9LFxufTtcblxuLy8gYGNvbnN0cnVjdG9yYCBpcyBub3QgZW51bWVyYWJsZS5cbk9iamVjdC5kZWZpbmVQcm9wZXJ0eShFdmVudFRhcmdldC5wcm90b3R5cGUsIFwiY29uc3RydWN0b3JcIiwge1xuICAgIHZhbHVlOiBFdmVudFRhcmdldCxcbiAgICBjb25maWd1cmFibGU6IHRydWUsXG4gICAgd3JpdGFibGU6IHRydWUsXG59KTtcblxuLy8gRW5zdXJlIGBldmVudFRhcmdldCBpbnN0YW5jZW9mIHdpbmRvdy5FdmVudFRhcmdldGAgaXMgYHRydWVgLlxuaWYgKFxuICAgIHR5cGVvZiB3aW5kb3cgIT09IFwidW5kZWZpbmVkXCIgJiZcbiAgICB0eXBlb2Ygd2luZG93LkV2ZW50VGFyZ2V0ICE9PSBcInVuZGVmaW5lZFwiXG4pIHtcbiAgICBPYmplY3Quc2V0UHJvdG90eXBlT2YoRXZlbnRUYXJnZXQucHJvdG90eXBlLCB3aW5kb3cuRXZlbnRUYXJnZXQucHJvdG90eXBlKTtcbn1cblxuZXhwb3J0cy5kZWZpbmVFdmVudEF0dHJpYnV0ZSA9IGRlZmluZUV2ZW50QXR0cmlidXRlO1xuZXhwb3J0cy5FdmVudFRhcmdldCA9IEV2ZW50VGFyZ2V0O1xuZXhwb3J0cy5kZWZhdWx0ID0gRXZlbnRUYXJnZXQ7XG5cbm1vZHVsZS5leHBvcnRzID0gRXZlbnRUYXJnZXRcbm1vZHVsZS5leHBvcnRzLkV2ZW50VGFyZ2V0ID0gbW9kdWxlLmV4cG9ydHNbXCJkZWZhdWx0XCJdID0gRXZlbnRUYXJnZXRcbm1vZHVsZS5leHBvcnRzLmRlZmluZUV2ZW50QXR0cmlidXRlID0gZGVmaW5lRXZlbnRBdHRyaWJ1dGVcbi8vIyBzb3VyY2VNYXBwaW5nVVJMPWV2ZW50LXRhcmdldC1zaGltLmpzLm1hcFxuIiwiY29uc3Qge1JlYWRhYmxlfSA9IHJlcXVpcmUoJ3N0cmVhbScpO1xuXG4vKipcbiAqIEB0eXBlIHtXZWFrTWFwPEJsb2IsIHt0eXBlOiBzdHJpbmcsIHNpemU6IG51bWJlciwgcGFydHM6IChCbG9iIHwgQnVmZmVyKVtdIH0+fVxuICovXG5jb25zdCB3bSA9IG5ldyBXZWFrTWFwKCk7XG5cbmFzeW5jIGZ1bmN0aW9uICogcmVhZChwYXJ0cykge1xuXHRmb3IgKGNvbnN0IHBhcnQgb2YgcGFydHMpIHtcblx0XHRpZiAoJ3N0cmVhbScgaW4gcGFydCkge1xuXHRcdFx0eWllbGQgKiBwYXJ0LnN0cmVhbSgpO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHR5aWVsZCBwYXJ0O1xuXHRcdH1cblx0fVxufVxuXG5jbGFzcyBCbG9iIHtcblx0LyoqXG5cdCAqIFRoZSBCbG9iKCkgY29uc3RydWN0b3IgcmV0dXJucyBhIG5ldyBCbG9iIG9iamVjdC4gVGhlIGNvbnRlbnRcblx0ICogb2YgdGhlIGJsb2IgY29uc2lzdHMgb2YgdGhlIGNvbmNhdGVuYXRpb24gb2YgdGhlIHZhbHVlcyBnaXZlblxuXHQgKiBpbiB0aGUgcGFyYW1ldGVyIGFycmF5LlxuXHQgKlxuXHQgKiBAcGFyYW0geyhBcnJheUJ1ZmZlckxpa2UgfCBBcnJheUJ1ZmZlclZpZXcgfCBCbG9iIHwgQnVmZmVyIHwgc3RyaW5nKVtdfSBibG9iUGFydHNcblx0ICogQHBhcmFtIHt7IHR5cGU/OiBzdHJpbmcgfX0gW29wdGlvbnNdXG5cdCAqL1xuXHRjb25zdHJ1Y3RvcihibG9iUGFydHMgPSBbXSwgb3B0aW9ucyA9IHt0eXBlOiAnJ30pIHtcblx0XHRsZXQgc2l6ZSA9IDA7XG5cblx0XHRjb25zdCBwYXJ0cyA9IGJsb2JQYXJ0cy5tYXAoZWxlbWVudCA9PiB7XG5cdFx0XHRsZXQgYnVmZmVyO1xuXHRcdFx0aWYgKGVsZW1lbnQgaW5zdGFuY2VvZiBCdWZmZXIpIHtcblx0XHRcdFx0YnVmZmVyID0gZWxlbWVudDtcblx0XHRcdH0gZWxzZSBpZiAoQXJyYXlCdWZmZXIuaXNWaWV3KGVsZW1lbnQpKSB7XG5cdFx0XHRcdGJ1ZmZlciA9IEJ1ZmZlci5mcm9tKGVsZW1lbnQuYnVmZmVyLCBlbGVtZW50LmJ5dGVPZmZzZXQsIGVsZW1lbnQuYnl0ZUxlbmd0aCk7XG5cdFx0XHR9IGVsc2UgaWYgKGVsZW1lbnQgaW5zdGFuY2VvZiBBcnJheUJ1ZmZlcikge1xuXHRcdFx0XHRidWZmZXIgPSBCdWZmZXIuZnJvbShlbGVtZW50KTtcblx0XHRcdH0gZWxzZSBpZiAoZWxlbWVudCBpbnN0YW5jZW9mIEJsb2IpIHtcblx0XHRcdFx0YnVmZmVyID0gZWxlbWVudDtcblx0XHRcdH0gZWxzZSB7XG5cdFx0XHRcdGJ1ZmZlciA9IEJ1ZmZlci5mcm9tKHR5cGVvZiBlbGVtZW50ID09PSAnc3RyaW5nJyA/IGVsZW1lbnQgOiBTdHJpbmcoZWxlbWVudCkpO1xuXHRcdFx0fVxuXG5cdFx0XHRzaXplICs9IGJ1ZmZlci5sZW5ndGggfHwgYnVmZmVyLnNpemUgfHwgMDtcblx0XHRcdHJldHVybiBidWZmZXI7XG5cdFx0fSk7XG5cblx0XHRjb25zdCB0eXBlID0gb3B0aW9ucy50eXBlID09PSB1bmRlZmluZWQgPyAnJyA6IFN0cmluZyhvcHRpb25zLnR5cGUpLnRvTG93ZXJDYXNlKCk7XG5cblx0XHR3bS5zZXQodGhpcywge1xuXHRcdFx0dHlwZTogL1teXFx1MDAyMC1cXHUwMDdFXS8udGVzdCh0eXBlKSA/ICcnIDogdHlwZSxcblx0XHRcdHNpemUsXG5cdFx0XHRwYXJ0c1xuXHRcdH0pO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBCbG9iIGludGVyZmFjZSdzIHNpemUgcHJvcGVydHkgcmV0dXJucyB0aGVcblx0ICogc2l6ZSBvZiB0aGUgQmxvYiBpbiBieXRlcy5cblx0ICovXG5cdGdldCBzaXplKCkge1xuXHRcdHJldHVybiB3bS5nZXQodGhpcykuc2l6ZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgdHlwZSBwcm9wZXJ0eSBvZiBhIEJsb2Igb2JqZWN0IHJldHVybnMgdGhlIE1JTUUgdHlwZSBvZiB0aGUgZmlsZS5cblx0ICovXG5cdGdldCB0eXBlKCkge1xuXHRcdHJldHVybiB3bS5nZXQodGhpcykudHlwZTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgdGV4dCgpIG1ldGhvZCBpbiB0aGUgQmxvYiBpbnRlcmZhY2UgcmV0dXJucyBhIFByb21pc2Vcblx0ICogdGhhdCByZXNvbHZlcyB3aXRoIGEgc3RyaW5nIGNvbnRhaW5pbmcgdGhlIGNvbnRlbnRzIG9mXG5cdCAqIHRoZSBibG9iLCBpbnRlcnByZXRlZCBhcyBVVEYtOC5cblx0ICpcblx0ICogQHJldHVybiB7UHJvbWlzZTxzdHJpbmc+fVxuXHQgKi9cblx0YXN5bmMgdGV4dCgpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmZyb20oYXdhaXQgdGhpcy5hcnJheUJ1ZmZlcigpKS50b1N0cmluZygpO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBhcnJheUJ1ZmZlcigpIG1ldGhvZCBpbiB0aGUgQmxvYiBpbnRlcmZhY2UgcmV0dXJucyBhXG5cdCAqIFByb21pc2UgdGhhdCByZXNvbHZlcyB3aXRoIHRoZSBjb250ZW50cyBvZiB0aGUgYmxvYiBhc1xuXHQgKiBiaW5hcnkgZGF0YSBjb250YWluZWQgaW4gYW4gQXJyYXlCdWZmZXIuXG5cdCAqXG5cdCAqIEByZXR1cm4ge1Byb21pc2U8QXJyYXlCdWZmZXI+fVxuXHQgKi9cblx0YXN5bmMgYXJyYXlCdWZmZXIoKSB7XG5cdFx0Y29uc3QgZGF0YSA9IG5ldyBVaW50OEFycmF5KHRoaXMuc2l6ZSk7XG5cdFx0bGV0IG9mZnNldCA9IDA7XG5cdFx0Zm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiB0aGlzLnN0cmVhbSgpKSB7XG5cdFx0XHRkYXRhLnNldChjaHVuaywgb2Zmc2V0KTtcblx0XHRcdG9mZnNldCArPSBjaHVuay5sZW5ndGg7XG5cdFx0fVxuXG5cdFx0cmV0dXJuIGRhdGEuYnVmZmVyO1xuXHR9XG5cblx0LyoqXG5cdCAqIFRoZSBCbG9iIGludGVyZmFjZSdzIHN0cmVhbSgpIG1ldGhvZCBpcyBkaWZmZXJlbmNlIGZyb20gbmF0aXZlXG5cdCAqIGFuZCB1c2VzIG5vZGUgc3RyZWFtcyBpbnN0ZWFkIG9mIHdoYXR3ZyBzdHJlYW1zLlxuXHQgKlxuXHQgKiBAcmV0dXJucyB7UmVhZGFibGV9IE5vZGUgcmVhZGFibGUgc3RyZWFtXG5cdCAqL1xuXHRzdHJlYW0oKSB7XG5cdFx0cmV0dXJuIFJlYWRhYmxlLmZyb20ocmVhZCh3bS5nZXQodGhpcykucGFydHMpKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBUaGUgQmxvYiBpbnRlcmZhY2UncyBzbGljZSgpIG1ldGhvZCBjcmVhdGVzIGFuZCByZXR1cm5zIGFcblx0ICogbmV3IEJsb2Igb2JqZWN0IHdoaWNoIGNvbnRhaW5zIGRhdGEgZnJvbSBhIHN1YnNldCBvZiB0aGVcblx0ICogYmxvYiBvbiB3aGljaCBpdCdzIGNhbGxlZC5cblx0ICpcblx0ICogQHBhcmFtIHtudW1iZXJ9IFtzdGFydF1cblx0ICogQHBhcmFtIHtudW1iZXJ9IFtlbmRdXG5cdCAqIEBwYXJhbSB7c3RyaW5nfSBbdHlwZV1cblx0ICovXG5cdHNsaWNlKHN0YXJ0ID0gMCwgZW5kID0gdGhpcy5zaXplLCB0eXBlID0gJycpIHtcblx0XHRjb25zdCB7c2l6ZX0gPSB0aGlzO1xuXG5cdFx0bGV0IHJlbGF0aXZlU3RhcnQgPSBzdGFydCA8IDAgPyBNYXRoLm1heChzaXplICsgc3RhcnQsIDApIDogTWF0aC5taW4oc3RhcnQsIHNpemUpO1xuXHRcdGxldCByZWxhdGl2ZUVuZCA9IGVuZCA8IDAgPyBNYXRoLm1heChzaXplICsgZW5kLCAwKSA6IE1hdGgubWluKGVuZCwgc2l6ZSk7XG5cblx0XHRjb25zdCBzcGFuID0gTWF0aC5tYXgocmVsYXRpdmVFbmQgLSByZWxhdGl2ZVN0YXJ0LCAwKTtcblx0XHRjb25zdCBwYXJ0cyA9IHdtLmdldCh0aGlzKS5wYXJ0cy52YWx1ZXMoKTtcblx0XHRjb25zdCBibG9iUGFydHMgPSBbXTtcblx0XHRsZXQgYWRkZWQgPSAwO1xuXG5cdFx0Zm9yIChjb25zdCBwYXJ0IG9mIHBhcnRzKSB7XG5cdFx0XHRjb25zdCBzaXplID0gQXJyYXlCdWZmZXIuaXNWaWV3KHBhcnQpID8gcGFydC5ieXRlTGVuZ3RoIDogcGFydC5zaXplO1xuXHRcdFx0aWYgKHJlbGF0aXZlU3RhcnQgJiYgc2l6ZSA8PSByZWxhdGl2ZVN0YXJ0KSB7XG5cdFx0XHRcdC8vIFNraXAgdGhlIGJlZ2lubmluZyBhbmQgY2hhbmdlIHRoZSByZWxhdGl2ZVxuXHRcdFx0XHQvLyBzdGFydCAmIGVuZCBwb3NpdGlvbiBhcyB3ZSBza2lwIHRoZSB1bndhbnRlZCBwYXJ0c1xuXHRcdFx0XHRyZWxhdGl2ZVN0YXJ0IC09IHNpemU7XG5cdFx0XHRcdHJlbGF0aXZlRW5kIC09IHNpemU7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRjb25zdCBjaHVuayA9IHBhcnQuc2xpY2UocmVsYXRpdmVTdGFydCwgTWF0aC5taW4oc2l6ZSwgcmVsYXRpdmVFbmQpKTtcblx0XHRcdFx0YmxvYlBhcnRzLnB1c2goY2h1bmspO1xuXHRcdFx0XHRhZGRlZCArPSBBcnJheUJ1ZmZlci5pc1ZpZXcoY2h1bmspID8gY2h1bmsuYnl0ZUxlbmd0aCA6IGNodW5rLnNpemU7XG5cdFx0XHRcdHJlbGF0aXZlU3RhcnQgPSAwOyAvLyBBbGwgbmV4dCBzZXF1ZW50YWwgcGFydHMgc2hvdWxkIHN0YXJ0IGF0IDBcblxuXHRcdFx0XHQvLyBkb24ndCBhZGQgdGhlIG92ZXJmbG93IHRvIG5ldyBibG9iUGFydHNcblx0XHRcdFx0aWYgKGFkZGVkID49IHNwYW4pIHtcblx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0fVxuXHRcdFx0fVxuXHRcdH1cblxuXHRcdGNvbnN0IGJsb2IgPSBuZXcgQmxvYihbXSwge3R5cGV9KTtcblx0XHRPYmplY3QuYXNzaWduKHdtLmdldChibG9iKSwge3NpemU6IHNwYW4sIHBhcnRzOiBibG9iUGFydHN9KTtcblxuXHRcdHJldHVybiBibG9iO1xuXHR9XG5cblx0Z2V0IFtTeW1ib2wudG9TdHJpbmdUYWddKCkge1xuXHRcdHJldHVybiAnQmxvYic7XG5cdH1cblxuXHRzdGF0aWMgW1N5bWJvbC5oYXNJbnN0YW5jZV0ob2JqZWN0KSB7XG5cdFx0cmV0dXJuIChcblx0XHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0XHR0eXBlb2Ygb2JqZWN0LnN0cmVhbSA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdFx0b2JqZWN0LnN0cmVhbS5sZW5ndGggPT09IDAgJiZcblx0XHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHRcdC9eKEJsb2J8RmlsZSkkLy50ZXN0KG9iamVjdFtTeW1ib2wudG9TdHJpbmdUYWddKVxuXHRcdCk7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoQmxvYi5wcm90b3R5cGUsIHtcblx0c2l6ZToge2VudW1lcmFibGU6IHRydWV9LFxuXHR0eXBlOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHNsaWNlOiB7ZW51bWVyYWJsZTogdHJ1ZX1cbn0pO1xuXG5tb2R1bGUuZXhwb3J0cyA9IEJsb2I7XG4iLCIndXNlIHN0cmljdCc7XG5jb25zdCBmZXRjaCA9IHJlcXVpcmUoJ25vZGUtZmV0Y2gnKTtcbmNvbnN0IEFib3J0Q29udHJvbGxlciA9IHJlcXVpcmUoJ2Fib3J0LWNvbnRyb2xsZXInKTtcblxuY29uc3QgVEVOX01FR0FCWVRFUyA9IDEwMDAgKiAxMDAwICogMTA7XG5cbmlmICghZ2xvYmFsLmZldGNoKSB7XG5cdGdsb2JhbC5mZXRjaCA9ICh1cmwsIG9wdGlvbnMpID0+IGZldGNoKHVybCwge2hpZ2hXYXRlck1hcms6IFRFTl9NRUdBQllURVMsIC4uLm9wdGlvbnN9KTtcbn1cblxuaWYgKCFnbG9iYWwuSGVhZGVycykge1xuXHRnbG9iYWwuSGVhZGVycyA9IGZldGNoLkhlYWRlcnM7XG59XG5cbmlmICghZ2xvYmFsLlJlcXVlc3QpIHtcblx0Z2xvYmFsLlJlcXVlc3QgPSBmZXRjaC5SZXF1ZXN0O1xufVxuXG5pZiAoIWdsb2JhbC5SZXNwb25zZSkge1xuXHRnbG9iYWwuUmVzcG9uc2UgPSBmZXRjaC5SZXNwb25zZTtcbn1cblxuaWYgKCFnbG9iYWwuQWJvcnRDb250cm9sbGVyKSB7XG5cdGdsb2JhbC5BYm9ydENvbnRyb2xsZXIgPSBBYm9ydENvbnRyb2xsZXI7XG59XG5cbmlmICghZ2xvYmFsLlJlYWRhYmxlU3RyZWFtKSB7XG5cdHRyeSB7XG5cdFx0Z2xvYmFsLlJlYWRhYmxlU3RyZWFtID0gcmVxdWlyZSgnd2ViLXN0cmVhbXMtcG9seWZpbGwvcG9ueWZpbGwvZXMyMDE4Jyk7XG5cdH0gY2F0Y2ggKF8pIHt9XG59XG5cbm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZSgna3kvdW1kJyk7XG4iLCIoZnVuY3Rpb24gKGdsb2JhbCwgZmFjdG9yeSkge1xuXHR0eXBlb2YgZXhwb3J0cyA9PT0gJ29iamVjdCcgJiYgdHlwZW9mIG1vZHVsZSAhPT0gJ3VuZGVmaW5lZCcgPyBtb2R1bGUuZXhwb3J0cyA9IGZhY3RvcnkoKSA6XG5cdHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZCA/IGRlZmluZShmYWN0b3J5KSA6XG5cdChnbG9iYWwgPSB0eXBlb2YgZ2xvYmFsVGhpcyAhPT0gJ3VuZGVmaW5lZCcgPyBnbG9iYWxUaGlzIDogZ2xvYmFsIHx8IHNlbGYsIGdsb2JhbC5reSA9IGZhY3RvcnkoKSk7XG59KHRoaXMsIChmdW5jdGlvbiAoKSB7ICd1c2Ugc3RyaWN0JztcblxuXHQvKiEgTUlUIExpY2Vuc2UgwqkgU2luZHJlIFNvcmh1cyAqL1xuXG5cdGNvbnN0IGdsb2JhbHMgPSB7fTtcblxuXHRjb25zdCBnZXRHbG9iYWwgPSBwcm9wZXJ0eSA9PiB7XG5cdFx0LyogaXN0YW5idWwgaWdub3JlIG5leHQgKi9cblx0XHRpZiAodHlwZW9mIHNlbGYgIT09ICd1bmRlZmluZWQnICYmIHNlbGYgJiYgcHJvcGVydHkgaW4gc2VsZikge1xuXHRcdFx0cmV0dXJuIHNlbGY7XG5cdFx0fVxuXG5cdFx0LyogaXN0YW5idWwgaWdub3JlIG5leHQgKi9cblx0XHRpZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcgJiYgd2luZG93ICYmIHByb3BlcnR5IGluIHdpbmRvdykge1xuXHRcdFx0cmV0dXJuIHdpbmRvdztcblx0XHR9XG5cblx0XHRpZiAodHlwZW9mIGdsb2JhbCAhPT0gJ3VuZGVmaW5lZCcgJiYgZ2xvYmFsICYmIHByb3BlcnR5IGluIGdsb2JhbCkge1xuXHRcdFx0cmV0dXJuIGdsb2JhbDtcblx0XHR9XG5cblx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdGlmICh0eXBlb2YgZ2xvYmFsVGhpcyAhPT0gJ3VuZGVmaW5lZCcgJiYgZ2xvYmFsVGhpcykge1xuXHRcdFx0cmV0dXJuIGdsb2JhbFRoaXM7XG5cdFx0fVxuXHR9O1xuXG5cdGNvbnN0IGdsb2JhbFByb3BlcnRpZXMgPSBbXG5cdFx0J0hlYWRlcnMnLFxuXHRcdCdSZXF1ZXN0Jyxcblx0XHQnUmVzcG9uc2UnLFxuXHRcdCdSZWFkYWJsZVN0cmVhbScsXG5cdFx0J2ZldGNoJyxcblx0XHQnQWJvcnRDb250cm9sbGVyJyxcblx0XHQnRm9ybURhdGEnXG5cdF07XG5cblx0Zm9yIChjb25zdCBwcm9wZXJ0eSBvZiBnbG9iYWxQcm9wZXJ0aWVzKSB7XG5cdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGdsb2JhbHMsIHByb3BlcnR5LCB7XG5cdFx0XHRnZXQoKSB7XG5cdFx0XHRcdGNvbnN0IGdsb2JhbE9iamVjdCA9IGdldEdsb2JhbChwcm9wZXJ0eSk7XG5cdFx0XHRcdGNvbnN0IHZhbHVlID0gZ2xvYmFsT2JqZWN0ICYmIGdsb2JhbE9iamVjdFtwcm9wZXJ0eV07XG5cdFx0XHRcdHJldHVybiB0eXBlb2YgdmFsdWUgPT09ICdmdW5jdGlvbicgPyB2YWx1ZS5iaW5kKGdsb2JhbE9iamVjdCkgOiB2YWx1ZTtcblx0XHRcdH1cblx0XHR9KTtcblx0fVxuXG5cdGNvbnN0IGlzT2JqZWN0ID0gdmFsdWUgPT4gdmFsdWUgIT09IG51bGwgJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0Jztcblx0Y29uc3Qgc3VwcG9ydHNBYm9ydENvbnRyb2xsZXIgPSB0eXBlb2YgZ2xvYmFscy5BYm9ydENvbnRyb2xsZXIgPT09ICdmdW5jdGlvbic7XG5cdGNvbnN0IHN1cHBvcnRzU3RyZWFtcyA9IHR5cGVvZiBnbG9iYWxzLlJlYWRhYmxlU3RyZWFtID09PSAnZnVuY3Rpb24nO1xuXHRjb25zdCBzdXBwb3J0c0Zvcm1EYXRhID0gdHlwZW9mIGdsb2JhbHMuRm9ybURhdGEgPT09ICdmdW5jdGlvbic7XG5cblx0Y29uc3QgbWVyZ2VIZWFkZXJzID0gKHNvdXJjZTEsIHNvdXJjZTIpID0+IHtcblx0XHRjb25zdCByZXN1bHQgPSBuZXcgZ2xvYmFscy5IZWFkZXJzKHNvdXJjZTEgfHwge30pO1xuXHRcdGNvbnN0IGlzSGVhZGVyc0luc3RhbmNlID0gc291cmNlMiBpbnN0YW5jZW9mIGdsb2JhbHMuSGVhZGVycztcblx0XHRjb25zdCBzb3VyY2UgPSBuZXcgZ2xvYmFscy5IZWFkZXJzKHNvdXJjZTIgfHwge30pO1xuXG5cdFx0Zm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2Ygc291cmNlKSB7XG5cdFx0XHRpZiAoKGlzSGVhZGVyc0luc3RhbmNlICYmIHZhbHVlID09PSAndW5kZWZpbmVkJykgfHwgdmFsdWUgPT09IHVuZGVmaW5lZCkge1xuXHRcdFx0XHRyZXN1bHQuZGVsZXRlKGtleSk7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRyZXN1bHQuc2V0KGtleSwgdmFsdWUpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHJldHVybiByZXN1bHQ7XG5cdH07XG5cblx0Y29uc3QgZGVlcE1lcmdlID0gKC4uLnNvdXJjZXMpID0+IHtcblx0XHRsZXQgcmV0dXJuVmFsdWUgPSB7fTtcblx0XHRsZXQgaGVhZGVycyA9IHt9O1xuXG5cdFx0Zm9yIChjb25zdCBzb3VyY2Ugb2Ygc291cmNlcykge1xuXHRcdFx0aWYgKEFycmF5LmlzQXJyYXkoc291cmNlKSkge1xuXHRcdFx0XHRpZiAoIShBcnJheS5pc0FycmF5KHJldHVyblZhbHVlKSkpIHtcblx0XHRcdFx0XHRyZXR1cm5WYWx1ZSA9IFtdO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0cmV0dXJuVmFsdWUgPSBbLi4ucmV0dXJuVmFsdWUsIC4uLnNvdXJjZV07XG5cdFx0XHR9IGVsc2UgaWYgKGlzT2JqZWN0KHNvdXJjZSkpIHtcblx0XHRcdFx0Zm9yIChsZXQgW2tleSwgdmFsdWVdIG9mIE9iamVjdC5lbnRyaWVzKHNvdXJjZSkpIHtcblx0XHRcdFx0XHRpZiAoaXNPYmplY3QodmFsdWUpICYmIChrZXkgaW4gcmV0dXJuVmFsdWUpKSB7XG5cdFx0XHRcdFx0XHR2YWx1ZSA9IGRlZXBNZXJnZShyZXR1cm5WYWx1ZVtrZXldLCB2YWx1ZSk7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0cmV0dXJuVmFsdWUgPSB7Li4ucmV0dXJuVmFsdWUsIFtrZXldOiB2YWx1ZX07XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpZiAoaXNPYmplY3Qoc291cmNlLmhlYWRlcnMpKSB7XG5cdFx0XHRcdFx0aGVhZGVycyA9IG1lcmdlSGVhZGVycyhoZWFkZXJzLCBzb3VyY2UuaGVhZGVycyk7XG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuVmFsdWUuaGVhZGVycyA9IGhlYWRlcnM7XG5cdFx0fVxuXG5cdFx0cmV0dXJuIHJldHVyblZhbHVlO1xuXHR9O1xuXG5cdGNvbnN0IHJlcXVlc3RNZXRob2RzID0gW1xuXHRcdCdnZXQnLFxuXHRcdCdwb3N0Jyxcblx0XHQncHV0Jyxcblx0XHQncGF0Y2gnLFxuXHRcdCdoZWFkJyxcblx0XHQnZGVsZXRlJ1xuXHRdO1xuXG5cdGNvbnN0IHJlc3BvbnNlVHlwZXMgPSB7XG5cdFx0anNvbjogJ2FwcGxpY2F0aW9uL2pzb24nLFxuXHRcdHRleHQ6ICd0ZXh0LyonLFxuXHRcdGZvcm1EYXRhOiAnbXVsdGlwYXJ0L2Zvcm0tZGF0YScsXG5cdFx0YXJyYXlCdWZmZXI6ICcqLyonLFxuXHRcdGJsb2I6ICcqLyonXG5cdH07XG5cblx0Y29uc3QgcmV0cnlNZXRob2RzID0gW1xuXHRcdCdnZXQnLFxuXHRcdCdwdXQnLFxuXHRcdCdoZWFkJyxcblx0XHQnZGVsZXRlJyxcblx0XHQnb3B0aW9ucycsXG5cdFx0J3RyYWNlJ1xuXHRdO1xuXG5cdGNvbnN0IHJldHJ5U3RhdHVzQ29kZXMgPSBbXG5cdFx0NDA4LFxuXHRcdDQxMyxcblx0XHQ0MjksXG5cdFx0NTAwLFxuXHRcdDUwMixcblx0XHQ1MDMsXG5cdFx0NTA0XG5cdF07XG5cblx0Y29uc3QgcmV0cnlBZnRlclN0YXR1c0NvZGVzID0gW1xuXHRcdDQxMyxcblx0XHQ0MjksXG5cdFx0NTAzXG5cdF07XG5cblx0Y29uc3Qgc3RvcCA9IFN5bWJvbCgnc3RvcCcpO1xuXG5cdGNsYXNzIEhUVFBFcnJvciBleHRlbmRzIEVycm9yIHtcblx0XHRjb25zdHJ1Y3RvcihyZXNwb25zZSkge1xuXHRcdFx0Ly8gU2V0IHRoZSBtZXNzYWdlIHRvIHRoZSBzdGF0dXMgdGV4dCwgc3VjaCBhcyBVbmF1dGhvcml6ZWQsXG5cdFx0XHQvLyB3aXRoIHNvbWUgZmFsbGJhY2tzLiBUaGlzIG1lc3NhZ2Ugc2hvdWxkIG5ldmVyIGJlIHVuZGVmaW5lZC5cblx0XHRcdHN1cGVyKFxuXHRcdFx0XHRyZXNwb25zZS5zdGF0dXNUZXh0IHx8XG5cdFx0XHRcdFN0cmluZyhcblx0XHRcdFx0XHQocmVzcG9uc2Uuc3RhdHVzID09PSAwIHx8IHJlc3BvbnNlLnN0YXR1cykgP1xuXHRcdFx0XHRcdFx0cmVzcG9uc2Uuc3RhdHVzIDogJ1Vua25vd24gcmVzcG9uc2UgZXJyb3InXG5cdFx0XHRcdClcblx0XHRcdCk7XG5cdFx0XHR0aGlzLm5hbWUgPSAnSFRUUEVycm9yJztcblx0XHRcdHRoaXMucmVzcG9uc2UgPSByZXNwb25zZTtcblx0XHR9XG5cdH1cblxuXHRjbGFzcyBUaW1lb3V0RXJyb3IgZXh0ZW5kcyBFcnJvciB7XG5cdFx0Y29uc3RydWN0b3IocmVxdWVzdCkge1xuXHRcdFx0c3VwZXIoJ1JlcXVlc3QgdGltZWQgb3V0Jyk7XG5cdFx0XHR0aGlzLm5hbWUgPSAnVGltZW91dEVycm9yJztcblx0XHRcdHRoaXMucmVxdWVzdCA9IHJlcXVlc3Q7XG5cdFx0fVxuXHR9XG5cblx0Y29uc3QgZGVsYXkgPSBtcyA9PiBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgbXMpKTtcblxuXHQvLyBgUHJvbWlzZS5yYWNlKClgIHdvcmthcm91bmQgKCM5MSlcblx0Y29uc3QgdGltZW91dCA9IChyZXF1ZXN0LCBhYm9ydENvbnRyb2xsZXIsIG9wdGlvbnMpID0+XG5cdFx0bmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuXHRcdFx0Y29uc3QgdGltZW91dElEID0gc2V0VGltZW91dCgoKSA9PiB7XG5cdFx0XHRcdGlmIChhYm9ydENvbnRyb2xsZXIpIHtcblx0XHRcdFx0XHRhYm9ydENvbnRyb2xsZXIuYWJvcnQoKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdHJlamVjdChuZXcgVGltZW91dEVycm9yKHJlcXVlc3QpKTtcblx0XHRcdH0sIG9wdGlvbnMudGltZW91dCk7XG5cblx0XHRcdC8qIGVzbGludC1kaXNhYmxlIHByb21pc2UvcHJlZmVyLWF3YWl0LXRvLXRoZW4gKi9cblx0XHRcdG9wdGlvbnMuZmV0Y2gocmVxdWVzdClcblx0XHRcdFx0LnRoZW4ocmVzb2x2ZSlcblx0XHRcdFx0LmNhdGNoKHJlamVjdClcblx0XHRcdFx0LnRoZW4oKCkgPT4ge1xuXHRcdFx0XHRcdGNsZWFyVGltZW91dCh0aW1lb3V0SUQpO1xuXHRcdFx0XHR9KTtcblx0XHRcdC8qIGVzbGludC1lbmFibGUgcHJvbWlzZS9wcmVmZXItYXdhaXQtdG8tdGhlbiAqL1xuXHRcdH0pO1xuXG5cdGNvbnN0IG5vcm1hbGl6ZVJlcXVlc3RNZXRob2QgPSBpbnB1dCA9PiByZXF1ZXN0TWV0aG9kcy5pbmNsdWRlcyhpbnB1dCkgPyBpbnB1dC50b1VwcGVyQ2FzZSgpIDogaW5wdXQ7XG5cblx0Y29uc3QgZGVmYXVsdFJldHJ5T3B0aW9ucyA9IHtcblx0XHRsaW1pdDogMixcblx0XHRtZXRob2RzOiByZXRyeU1ldGhvZHMsXG5cdFx0c3RhdHVzQ29kZXM6IHJldHJ5U3RhdHVzQ29kZXMsXG5cdFx0YWZ0ZXJTdGF0dXNDb2RlczogcmV0cnlBZnRlclN0YXR1c0NvZGVzXG5cdH07XG5cblx0Y29uc3Qgbm9ybWFsaXplUmV0cnlPcHRpb25zID0gKHJldHJ5ID0ge30pID0+IHtcblx0XHRpZiAodHlwZW9mIHJldHJ5ID09PSAnbnVtYmVyJykge1xuXHRcdFx0cmV0dXJuIHtcblx0XHRcdFx0Li4uZGVmYXVsdFJldHJ5T3B0aW9ucyxcblx0XHRcdFx0bGltaXQ6IHJldHJ5XG5cdFx0XHR9O1xuXHRcdH1cblxuXHRcdGlmIChyZXRyeS5tZXRob2RzICYmICFBcnJheS5pc0FycmF5KHJldHJ5Lm1ldGhvZHMpKSB7XG5cdFx0XHR0aHJvdyBuZXcgRXJyb3IoJ3JldHJ5Lm1ldGhvZHMgbXVzdCBiZSBhbiBhcnJheScpO1xuXHRcdH1cblxuXHRcdGlmIChyZXRyeS5zdGF0dXNDb2RlcyAmJiAhQXJyYXkuaXNBcnJheShyZXRyeS5zdGF0dXNDb2RlcykpIHtcblx0XHRcdHRocm93IG5ldyBFcnJvcigncmV0cnkuc3RhdHVzQ29kZXMgbXVzdCBiZSBhbiBhcnJheScpO1xuXHRcdH1cblxuXHRcdHJldHVybiB7XG5cdFx0XHQuLi5kZWZhdWx0UmV0cnlPcHRpb25zLFxuXHRcdFx0Li4ucmV0cnksXG5cdFx0XHRhZnRlclN0YXR1c0NvZGVzOiByZXRyeUFmdGVyU3RhdHVzQ29kZXNcblx0XHR9O1xuXHR9O1xuXG5cdC8vIFRoZSBtYXhpbXVtIHZhbHVlIG9mIGEgMzJiaXQgaW50IChzZWUgaXNzdWUgIzExNylcblx0Y29uc3QgbWF4U2FmZVRpbWVvdXQgPSAyMTQ3NDgzNjQ3O1xuXG5cdGNsYXNzIEt5IHtcblx0XHRjb25zdHJ1Y3RvcihpbnB1dCwgb3B0aW9ucyA9IHt9KSB7XG5cdFx0XHR0aGlzLl9yZXRyeUNvdW50ID0gMDtcblx0XHRcdHRoaXMuX2lucHV0ID0gaW5wdXQ7XG5cdFx0XHR0aGlzLl9vcHRpb25zID0ge1xuXHRcdFx0XHQvLyBUT0RPOiBjcmVkZW50aWFscyBjYW4gYmUgcmVtb3ZlZCB3aGVuIHRoZSBzcGVjIGNoYW5nZSBpcyBpbXBsZW1lbnRlZCBpbiBhbGwgYnJvd3NlcnMuIENvbnRleHQ6IGh0dHBzOi8vd3d3LmNocm9tZXN0YXR1cy5jb20vZmVhdHVyZS80NTM5NDczMzEyMzUwMjA4XG5cdFx0XHRcdGNyZWRlbnRpYWxzOiB0aGlzLl9pbnB1dC5jcmVkZW50aWFscyB8fCAnc2FtZS1vcmlnaW4nLFxuXHRcdFx0XHQuLi5vcHRpb25zLFxuXHRcdFx0XHRoZWFkZXJzOiBtZXJnZUhlYWRlcnModGhpcy5faW5wdXQuaGVhZGVycywgb3B0aW9ucy5oZWFkZXJzKSxcblx0XHRcdFx0aG9va3M6IGRlZXBNZXJnZSh7XG5cdFx0XHRcdFx0YmVmb3JlUmVxdWVzdDogW10sXG5cdFx0XHRcdFx0YmVmb3JlUmV0cnk6IFtdLFxuXHRcdFx0XHRcdGFmdGVyUmVzcG9uc2U6IFtdXG5cdFx0XHRcdH0sIG9wdGlvbnMuaG9va3MpLFxuXHRcdFx0XHRtZXRob2Q6IG5vcm1hbGl6ZVJlcXVlc3RNZXRob2Qob3B0aW9ucy5tZXRob2QgfHwgdGhpcy5faW5wdXQubWV0aG9kKSxcblx0XHRcdFx0cHJlZml4VXJsOiBTdHJpbmcob3B0aW9ucy5wcmVmaXhVcmwgfHwgJycpLFxuXHRcdFx0XHRyZXRyeTogbm9ybWFsaXplUmV0cnlPcHRpb25zKG9wdGlvbnMucmV0cnkpLFxuXHRcdFx0XHR0aHJvd0h0dHBFcnJvcnM6IG9wdGlvbnMudGhyb3dIdHRwRXJyb3JzICE9PSBmYWxzZSxcblx0XHRcdFx0dGltZW91dDogdHlwZW9mIG9wdGlvbnMudGltZW91dCA9PT0gJ3VuZGVmaW5lZCcgPyAxMDAwMCA6IG9wdGlvbnMudGltZW91dCxcblx0XHRcdFx0ZmV0Y2g6IG9wdGlvbnMuZmV0Y2ggfHwgZ2xvYmFscy5mZXRjaFxuXHRcdFx0fTtcblxuXHRcdFx0aWYgKHR5cGVvZiB0aGlzLl9pbnB1dCAhPT0gJ3N0cmluZycgJiYgISh0aGlzLl9pbnB1dCBpbnN0YW5jZW9mIFVSTCB8fCB0aGlzLl9pbnB1dCBpbnN0YW5jZW9mIGdsb2JhbHMuUmVxdWVzdCkpIHtcblx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignYGlucHV0YCBtdXN0IGJlIGEgc3RyaW5nLCBVUkwsIG9yIFJlcXVlc3QnKTtcblx0XHRcdH1cblxuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMucHJlZml4VXJsICYmIHR5cGVvZiB0aGlzLl9pbnB1dCA9PT0gJ3N0cmluZycpIHtcblx0XHRcdFx0aWYgKHRoaXMuX2lucHV0LnN0YXJ0c1dpdGgoJy8nKSkge1xuXHRcdFx0XHRcdHRocm93IG5ldyBFcnJvcignYGlucHV0YCBtdXN0IG5vdCBiZWdpbiB3aXRoIGEgc2xhc2ggd2hlbiB1c2luZyBgcHJlZml4VXJsYCcpO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0aWYgKCF0aGlzLl9vcHRpb25zLnByZWZpeFVybC5lbmRzV2l0aCgnLycpKSB7XG5cdFx0XHRcdFx0dGhpcy5fb3B0aW9ucy5wcmVmaXhVcmwgKz0gJy8nO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0dGhpcy5faW5wdXQgPSB0aGlzLl9vcHRpb25zLnByZWZpeFVybCArIHRoaXMuX2lucHV0O1xuXHRcdFx0fVxuXG5cdFx0XHRpZiAoc3VwcG9ydHNBYm9ydENvbnRyb2xsZXIpIHtcblx0XHRcdFx0dGhpcy5hYm9ydENvbnRyb2xsZXIgPSBuZXcgZ2xvYmFscy5BYm9ydENvbnRyb2xsZXIoKTtcblx0XHRcdFx0aWYgKHRoaXMuX29wdGlvbnMuc2lnbmFsKSB7XG5cdFx0XHRcdFx0dGhpcy5fb3B0aW9ucy5zaWduYWwuYWRkRXZlbnRMaXN0ZW5lcignYWJvcnQnLCAoKSA9PiB7XG5cdFx0XHRcdFx0XHR0aGlzLmFib3J0Q29udHJvbGxlci5hYm9ydCgpO1xuXHRcdFx0XHRcdH0pO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0dGhpcy5fb3B0aW9ucy5zaWduYWwgPSB0aGlzLmFib3J0Q29udHJvbGxlci5zaWduYWw7XG5cdFx0XHR9XG5cblx0XHRcdHRoaXMucmVxdWVzdCA9IG5ldyBnbG9iYWxzLlJlcXVlc3QodGhpcy5faW5wdXQsIHRoaXMuX29wdGlvbnMpO1xuXG5cdFx0XHRpZiAodGhpcy5fb3B0aW9ucy5zZWFyY2hQYXJhbXMpIHtcblx0XHRcdFx0Y29uc3Qgc2VhcmNoUGFyYW1zID0gJz8nICsgbmV3IFVSTFNlYXJjaFBhcmFtcyh0aGlzLl9vcHRpb25zLnNlYXJjaFBhcmFtcykudG9TdHJpbmcoKTtcblx0XHRcdFx0Y29uc3QgdXJsID0gdGhpcy5yZXF1ZXN0LnVybC5yZXBsYWNlKC8oPzpcXD8uKj8pPyg/PSN8JCkvLCBzZWFyY2hQYXJhbXMpO1xuXG5cdFx0XHRcdC8vIFRvIHByb3ZpZGUgY29ycmVjdCBmb3JtIGJvdW5kYXJ5LCBDb250ZW50LVR5cGUgaGVhZGVyIHNob3VsZCBiZSBkZWxldGVkIGVhY2ggdGltZSB3aGVuIG5ldyBSZXF1ZXN0IGluc3RhbnRpYXRlZCBmcm9tIGFub3RoZXIgb25lXG5cdFx0XHRcdGlmICgoKHN1cHBvcnRzRm9ybURhdGEgJiYgdGhpcy5fb3B0aW9ucy5ib2R5IGluc3RhbmNlb2YgZ2xvYmFscy5Gb3JtRGF0YSkgfHwgdGhpcy5fb3B0aW9ucy5ib2R5IGluc3RhbmNlb2YgVVJMU2VhcmNoUGFyYW1zKSAmJiAhKHRoaXMuX29wdGlvbnMuaGVhZGVycyAmJiB0aGlzLl9vcHRpb25zLmhlYWRlcnNbJ2NvbnRlbnQtdHlwZSddKSkge1xuXHRcdFx0XHRcdHRoaXMucmVxdWVzdC5oZWFkZXJzLmRlbGV0ZSgnY29udGVudC10eXBlJyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR0aGlzLnJlcXVlc3QgPSBuZXcgZ2xvYmFscy5SZXF1ZXN0KG5ldyBnbG9iYWxzLlJlcXVlc3QodXJsLCB0aGlzLnJlcXVlc3QpLCB0aGlzLl9vcHRpb25zKTtcblx0XHRcdH1cblxuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMuanNvbiAhPT0gdW5kZWZpbmVkKSB7XG5cdFx0XHRcdHRoaXMuX29wdGlvbnMuYm9keSA9IEpTT04uc3RyaW5naWZ5KHRoaXMuX29wdGlvbnMuanNvbik7XG5cdFx0XHRcdHRoaXMucmVxdWVzdC5oZWFkZXJzLnNldCgnY29udGVudC10eXBlJywgJ2FwcGxpY2F0aW9uL2pzb24nKTtcblx0XHRcdFx0dGhpcy5yZXF1ZXN0ID0gbmV3IGdsb2JhbHMuUmVxdWVzdCh0aGlzLnJlcXVlc3QsIHtib2R5OiB0aGlzLl9vcHRpb25zLmJvZHl9KTtcblx0XHRcdH1cblxuXHRcdFx0Y29uc3QgZm4gPSBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRpbWVvdXQgPiBtYXhTYWZlVGltZW91dCkge1xuXHRcdFx0XHRcdHRocm93IG5ldyBSYW5nZUVycm9yKGBUaGUgXFxgdGltZW91dFxcYCBvcHRpb24gY2Fubm90IGJlIGdyZWF0ZXIgdGhhbiAke21heFNhZmVUaW1lb3V0fWApO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0YXdhaXQgZGVsYXkoMSk7XG5cdFx0XHRcdGxldCByZXNwb25zZSA9IGF3YWl0IHRoaXMuX2ZldGNoKCk7XG5cblx0XHRcdFx0Zm9yIChjb25zdCBob29rIG9mIHRoaXMuX29wdGlvbnMuaG9va3MuYWZ0ZXJSZXNwb25zZSkge1xuXHRcdFx0XHRcdC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1hd2FpdC1pbi1sb29wXG5cdFx0XHRcdFx0Y29uc3QgbW9kaWZpZWRSZXNwb25zZSA9IGF3YWl0IGhvb2soXG5cdFx0XHRcdFx0XHR0aGlzLnJlcXVlc3QsXG5cdFx0XHRcdFx0XHR0aGlzLl9vcHRpb25zLFxuXHRcdFx0XHRcdFx0dGhpcy5fZGVjb3JhdGVSZXNwb25zZShyZXNwb25zZS5jbG9uZSgpKVxuXHRcdFx0XHRcdCk7XG5cblx0XHRcdFx0XHRpZiAobW9kaWZpZWRSZXNwb25zZSBpbnN0YW5jZW9mIGdsb2JhbHMuUmVzcG9uc2UpIHtcblx0XHRcdFx0XHRcdHJlc3BvbnNlID0gbW9kaWZpZWRSZXNwb25zZTtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblxuXHRcdFx0XHR0aGlzLl9kZWNvcmF0ZVJlc3BvbnNlKHJlc3BvbnNlKTtcblxuXHRcdFx0XHRpZiAoIXJlc3BvbnNlLm9rICYmIHRoaXMuX29wdGlvbnMudGhyb3dIdHRwRXJyb3JzKSB7XG5cdFx0XHRcdFx0dGhyb3cgbmV3IEhUVFBFcnJvcihyZXNwb25zZSk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHQvLyBJZiBgb25Eb3dubG9hZFByb2dyZXNzYCBpcyBwYXNzZWQsIGl0IHVzZXMgdGhlIHN0cmVhbSBBUEkgaW50ZXJuYWxseVxuXHRcdFx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdFx0XHRpZiAodGhpcy5fb3B0aW9ucy5vbkRvd25sb2FkUHJvZ3Jlc3MpIHtcblx0XHRcdFx0XHRpZiAodHlwZW9mIHRoaXMuX29wdGlvbnMub25Eb3dubG9hZFByb2dyZXNzICE9PSAnZnVuY3Rpb24nKSB7XG5cdFx0XHRcdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgYG9uRG93bmxvYWRQcm9ncmVzc2Agb3B0aW9uIG11c3QgYmUgYSBmdW5jdGlvbicpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdGlmICghc3VwcG9ydHNTdHJlYW1zKSB7XG5cdFx0XHRcdFx0XHR0aHJvdyBuZXcgRXJyb3IoJ1N0cmVhbXMgYXJlIG5vdCBzdXBwb3J0ZWQgaW4geW91ciBlbnZpcm9ubWVudC4gYFJlYWRhYmxlU3RyZWFtYCBpcyBtaXNzaW5nLicpO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJldHVybiB0aGlzLl9zdHJlYW0ocmVzcG9uc2UuY2xvbmUoKSwgdGhpcy5fb3B0aW9ucy5vbkRvd25sb2FkUHJvZ3Jlc3MpO1xuXHRcdFx0XHR9XG5cblx0XHRcdFx0cmV0dXJuIHJlc3BvbnNlO1xuXHRcdFx0fTtcblxuXHRcdFx0Y29uc3QgaXNSZXRyaWFibGVNZXRob2QgPSB0aGlzLl9vcHRpb25zLnJldHJ5Lm1ldGhvZHMuaW5jbHVkZXModGhpcy5yZXF1ZXN0Lm1ldGhvZC50b0xvd2VyQ2FzZSgpKTtcblx0XHRcdGNvbnN0IHJlc3VsdCA9IGlzUmV0cmlhYmxlTWV0aG9kID8gdGhpcy5fcmV0cnkoZm4pIDogZm4oKTtcblxuXHRcdFx0Zm9yIChjb25zdCBbdHlwZSwgbWltZVR5cGVdIG9mIE9iamVjdC5lbnRyaWVzKHJlc3BvbnNlVHlwZXMpKSB7XG5cdFx0XHRcdHJlc3VsdFt0eXBlXSA9IGFzeW5jICgpID0+IHtcblx0XHRcdFx0XHR0aGlzLnJlcXVlc3QuaGVhZGVycy5zZXQoJ2FjY2VwdCcsIHRoaXMucmVxdWVzdC5oZWFkZXJzLmdldCgnYWNjZXB0JykgfHwgbWltZVR5cGUpO1xuXG5cdFx0XHRcdFx0Y29uc3QgcmVzcG9uc2UgPSAoYXdhaXQgcmVzdWx0KS5jbG9uZSgpO1xuXG5cdFx0XHRcdFx0aWYgKHR5cGUgPT09ICdqc29uJykge1xuXHRcdFx0XHRcdFx0aWYgKHJlc3BvbnNlLnN0YXR1cyA9PT0gMjA0KSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiAnJztcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0aWYgKG9wdGlvbnMucGFyc2VKc29uKSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiBvcHRpb25zLnBhcnNlSnNvbihhd2FpdCByZXNwb25zZS50ZXh0KCkpO1xuXHRcdFx0XHRcdFx0fVxuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJldHVybiByZXNwb25zZVt0eXBlXSgpO1xuXHRcdFx0XHR9O1xuXHRcdFx0fVxuXG5cdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdH1cblxuXHRcdF9jYWxjdWxhdGVSZXRyeURlbGF5KGVycm9yKSB7XG5cdFx0XHR0aGlzLl9yZXRyeUNvdW50Kys7XG5cblx0XHRcdGlmICh0aGlzLl9yZXRyeUNvdW50IDwgdGhpcy5fb3B0aW9ucy5yZXRyeS5saW1pdCAmJiAhKGVycm9yIGluc3RhbmNlb2YgVGltZW91dEVycm9yKSkge1xuXHRcdFx0XHRpZiAoZXJyb3IgaW5zdGFuY2VvZiBIVFRQRXJyb3IpIHtcblx0XHRcdFx0XHRpZiAoIXRoaXMuX29wdGlvbnMucmV0cnkuc3RhdHVzQ29kZXMuaW5jbHVkZXMoZXJyb3IucmVzcG9uc2Uuc3RhdHVzKSkge1xuXHRcdFx0XHRcdFx0cmV0dXJuIDA7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0Y29uc3QgcmV0cnlBZnRlciA9IGVycm9yLnJlc3BvbnNlLmhlYWRlcnMuZ2V0KCdSZXRyeS1BZnRlcicpO1xuXHRcdFx0XHRcdGlmIChyZXRyeUFmdGVyICYmIHRoaXMuX29wdGlvbnMucmV0cnkuYWZ0ZXJTdGF0dXNDb2Rlcy5pbmNsdWRlcyhlcnJvci5yZXNwb25zZS5zdGF0dXMpKSB7XG5cdFx0XHRcdFx0XHRsZXQgYWZ0ZXIgPSBOdW1iZXIocmV0cnlBZnRlcik7XG5cdFx0XHRcdFx0XHRpZiAoTnVtYmVyLmlzTmFOKGFmdGVyKSkge1xuXHRcdFx0XHRcdFx0XHRhZnRlciA9IERhdGUucGFyc2UocmV0cnlBZnRlcikgLSBEYXRlLm5vdygpO1xuXHRcdFx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHRcdFx0YWZ0ZXIgKj0gMTAwMDtcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0aWYgKHR5cGVvZiB0aGlzLl9vcHRpb25zLnJldHJ5Lm1heFJldHJ5QWZ0ZXIgIT09ICd1bmRlZmluZWQnICYmIGFmdGVyID4gdGhpcy5fb3B0aW9ucy5yZXRyeS5tYXhSZXRyeUFmdGVyKSB7XG5cdFx0XHRcdFx0XHRcdHJldHVybiAwO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZXR1cm4gYWZ0ZXI7XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0aWYgKGVycm9yLnJlc3BvbnNlLnN0YXR1cyA9PT0gNDEzKSB7XG5cdFx0XHRcdFx0XHRyZXR1cm4gMDtcblx0XHRcdFx0XHR9XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRjb25zdCBCQUNLT0ZGX0ZBQ1RPUiA9IDAuMztcblx0XHRcdFx0cmV0dXJuIEJBQ0tPRkZfRkFDVE9SICogKDIgKiogKHRoaXMuX3JldHJ5Q291bnQgLSAxKSkgKiAxMDAwO1xuXHRcdFx0fVxuXG5cdFx0XHRyZXR1cm4gMDtcblx0XHR9XG5cblx0XHRfZGVjb3JhdGVSZXNwb25zZShyZXNwb25zZSkge1xuXHRcdFx0aWYgKHRoaXMuX29wdGlvbnMucGFyc2VKc29uKSB7XG5cdFx0XHRcdHJlc3BvbnNlLmpzb24gPSBhc3luYyAoKSA9PiB7XG5cdFx0XHRcdFx0cmV0dXJuIHRoaXMuX29wdGlvbnMucGFyc2VKc29uKGF3YWl0IHJlc3BvbnNlLnRleHQoKSk7XG5cdFx0XHRcdH07XG5cdFx0XHR9XG5cblx0XHRcdHJldHVybiByZXNwb25zZTtcblx0XHR9XG5cblx0XHRhc3luYyBfcmV0cnkoZm4pIHtcblx0XHRcdHRyeSB7XG5cdFx0XHRcdHJldHVybiBhd2FpdCBmbigpO1xuXHRcdFx0fSBjYXRjaCAoZXJyb3IpIHtcblx0XHRcdFx0Y29uc3QgbXMgPSBNYXRoLm1pbih0aGlzLl9jYWxjdWxhdGVSZXRyeURlbGF5KGVycm9yKSwgbWF4U2FmZVRpbWVvdXQpO1xuXHRcdFx0XHRpZiAobXMgIT09IDAgJiYgdGhpcy5fcmV0cnlDb3VudCA+IDApIHtcblx0XHRcdFx0XHRhd2FpdCBkZWxheShtcyk7XG5cblx0XHRcdFx0XHRmb3IgKGNvbnN0IGhvb2sgb2YgdGhpcy5fb3B0aW9ucy5ob29rcy5iZWZvcmVSZXRyeSkge1xuXHRcdFx0XHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWF3YWl0LWluLWxvb3Bcblx0XHRcdFx0XHRcdGNvbnN0IGhvb2tSZXN1bHQgPSBhd2FpdCBob29rKHtcblx0XHRcdFx0XHRcdFx0cmVxdWVzdDogdGhpcy5yZXF1ZXN0LFxuXHRcdFx0XHRcdFx0XHRvcHRpb25zOiB0aGlzLl9vcHRpb25zLFxuXHRcdFx0XHRcdFx0XHRlcnJvcixcblx0XHRcdFx0XHRcdFx0cmV0cnlDb3VudDogdGhpcy5fcmV0cnlDb3VudFxuXHRcdFx0XHRcdFx0fSk7XG5cblx0XHRcdFx0XHRcdC8vIElmIGBzdG9wYCBpcyByZXR1cm5lZCBmcm9tIHRoZSBob29rLCB0aGUgcmV0cnkgcHJvY2VzcyBpcyBzdG9wcGVkXG5cdFx0XHRcdFx0XHRpZiAoaG9va1Jlc3VsdCA9PT0gc3RvcCkge1xuXHRcdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0XHR9XG5cdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0cmV0dXJuIHRoaXMuX3JldHJ5KGZuKTtcblx0XHRcdFx0fVxuXG5cdFx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRocm93SHR0cEVycm9ycykge1xuXHRcdFx0XHRcdHRocm93IGVycm9yO1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cdFx0fVxuXG5cdFx0YXN5bmMgX2ZldGNoKCkge1xuXHRcdFx0Zm9yIChjb25zdCBob29rIG9mIHRoaXMuX29wdGlvbnMuaG9va3MuYmVmb3JlUmVxdWVzdCkge1xuXHRcdFx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tYXdhaXQtaW4tbG9vcFxuXHRcdFx0XHRjb25zdCByZXN1bHQgPSBhd2FpdCBob29rKHRoaXMucmVxdWVzdCwgdGhpcy5fb3B0aW9ucyk7XG5cblx0XHRcdFx0aWYgKHJlc3VsdCBpbnN0YW5jZW9mIFJlcXVlc3QpIHtcblx0XHRcdFx0XHR0aGlzLnJlcXVlc3QgPSByZXN1bHQ7XG5cdFx0XHRcdFx0YnJlYWs7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRpZiAocmVzdWx0IGluc3RhbmNlb2YgUmVzcG9uc2UpIHtcblx0XHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0XHR9XG5cdFx0XHR9XG5cblx0XHRcdGlmICh0aGlzLl9vcHRpb25zLnRpbWVvdXQgPT09IGZhbHNlKSB7XG5cdFx0XHRcdHJldHVybiB0aGlzLl9vcHRpb25zLmZldGNoKHRoaXMucmVxdWVzdC5jbG9uZSgpKTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIHRpbWVvdXQodGhpcy5yZXF1ZXN0LmNsb25lKCksIHRoaXMuYWJvcnRDb250cm9sbGVyLCB0aGlzLl9vcHRpb25zKTtcblx0XHR9XG5cblx0XHQvKiBpc3RhbmJ1bCBpZ25vcmUgbmV4dCAqL1xuXHRcdF9zdHJlYW0ocmVzcG9uc2UsIG9uRG93bmxvYWRQcm9ncmVzcykge1xuXHRcdFx0Y29uc3QgdG90YWxCeXRlcyA9IE51bWJlcihyZXNwb25zZS5oZWFkZXJzLmdldCgnY29udGVudC1sZW5ndGgnKSkgfHwgMDtcblx0XHRcdGxldCB0cmFuc2ZlcnJlZEJ5dGVzID0gMDtcblxuXHRcdFx0cmV0dXJuIG5ldyBnbG9iYWxzLlJlc3BvbnNlKFxuXHRcdFx0XHRuZXcgZ2xvYmFscy5SZWFkYWJsZVN0cmVhbSh7XG5cdFx0XHRcdFx0c3RhcnQoY29udHJvbGxlcikge1xuXHRcdFx0XHRcdFx0Y29uc3QgcmVhZGVyID0gcmVzcG9uc2UuYm9keS5nZXRSZWFkZXIoKTtcblxuXHRcdFx0XHRcdFx0aWYgKG9uRG93bmxvYWRQcm9ncmVzcykge1xuXHRcdFx0XHRcdFx0XHRvbkRvd25sb2FkUHJvZ3Jlc3Moe3BlcmNlbnQ6IDAsIHRyYW5zZmVycmVkQnl0ZXM6IDAsIHRvdGFsQnl0ZXN9LCBuZXcgVWludDhBcnJheSgpKTtcblx0XHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdFx0YXN5bmMgZnVuY3Rpb24gcmVhZCgpIHtcblx0XHRcdFx0XHRcdFx0Y29uc3Qge2RvbmUsIHZhbHVlfSA9IGF3YWl0IHJlYWRlci5yZWFkKCk7XG5cdFx0XHRcdFx0XHRcdGlmIChkb25lKSB7XG5cdFx0XHRcdFx0XHRcdFx0Y29udHJvbGxlci5jbG9zZSgpO1xuXHRcdFx0XHRcdFx0XHRcdHJldHVybjtcblx0XHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRcdGlmIChvbkRvd25sb2FkUHJvZ3Jlc3MpIHtcblx0XHRcdFx0XHRcdFx0XHR0cmFuc2ZlcnJlZEJ5dGVzICs9IHZhbHVlLmJ5dGVMZW5ndGg7XG5cdFx0XHRcdFx0XHRcdFx0Y29uc3QgcGVyY2VudCA9IHRvdGFsQnl0ZXMgPT09IDAgPyAwIDogdHJhbnNmZXJyZWRCeXRlcyAvIHRvdGFsQnl0ZXM7XG5cdFx0XHRcdFx0XHRcdFx0b25Eb3dubG9hZFByb2dyZXNzKHtwZXJjZW50LCB0cmFuc2ZlcnJlZEJ5dGVzLCB0b3RhbEJ5dGVzfSwgdmFsdWUpO1xuXHRcdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdFx0Y29udHJvbGxlci5lbnF1ZXVlKHZhbHVlKTtcblx0XHRcdFx0XHRcdFx0cmVhZCgpO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZWFkKCk7XG5cdFx0XHRcdFx0fVxuXHRcdFx0XHR9KVxuXHRcdFx0KTtcblx0XHR9XG5cdH1cblxuXHRjb25zdCB2YWxpZGF0ZUFuZE1lcmdlID0gKC4uLnNvdXJjZXMpID0+IHtcblx0XHRmb3IgKGNvbnN0IHNvdXJjZSBvZiBzb3VyY2VzKSB7XG5cdFx0XHRpZiAoKCFpc09iamVjdChzb3VyY2UpIHx8IEFycmF5LmlzQXJyYXkoc291cmNlKSkgJiYgdHlwZW9mIHNvdXJjZSAhPT0gJ3VuZGVmaW5lZCcpIHtcblx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignVGhlIGBvcHRpb25zYCBhcmd1bWVudCBtdXN0IGJlIGFuIG9iamVjdCcpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHJldHVybiBkZWVwTWVyZ2Uoe30sIC4uLnNvdXJjZXMpO1xuXHR9O1xuXG5cdGNvbnN0IGNyZWF0ZUluc3RhbmNlID0gZGVmYXVsdHMgPT4ge1xuXHRcdGNvbnN0IGt5ID0gKGlucHV0LCBvcHRpb25zKSA9PiBuZXcgS3koaW5wdXQsIHZhbGlkYXRlQW5kTWVyZ2UoZGVmYXVsdHMsIG9wdGlvbnMpKTtcblxuXHRcdGZvciAoY29uc3QgbWV0aG9kIG9mIHJlcXVlc3RNZXRob2RzKSB7XG5cdFx0XHRreVttZXRob2RdID0gKGlucHV0LCBvcHRpb25zKSA9PiBuZXcgS3koaW5wdXQsIHZhbGlkYXRlQW5kTWVyZ2UoZGVmYXVsdHMsIG9wdGlvbnMsIHttZXRob2R9KSk7XG5cdFx0fVxuXG5cdFx0a3kuSFRUUEVycm9yID0gSFRUUEVycm9yO1xuXHRcdGt5LlRpbWVvdXRFcnJvciA9IFRpbWVvdXRFcnJvcjtcblx0XHRreS5jcmVhdGUgPSBuZXdEZWZhdWx0cyA9PiBjcmVhdGVJbnN0YW5jZSh2YWxpZGF0ZUFuZE1lcmdlKG5ld0RlZmF1bHRzKSk7XG5cdFx0a3kuZXh0ZW5kID0gbmV3RGVmYXVsdHMgPT4gY3JlYXRlSW5zdGFuY2UodmFsaWRhdGVBbmRNZXJnZShkZWZhdWx0cywgbmV3RGVmYXVsdHMpKTtcblx0XHRreS5zdG9wID0gc3RvcDtcblxuXHRcdHJldHVybiBreTtcblx0fTtcblxuXHR2YXIgaW5kZXggPSBjcmVhdGVJbnN0YW5jZSgpO1xuXG5cdHJldHVybiBpbmRleDtcblxufSkpKTtcbiIsIid1c2Ugc3RyaWN0JztcblxuZXhwb3J0cyA9IG1vZHVsZS5leHBvcnRzID0gZmV0Y2g7XG5cbmNvbnN0IGh0dHAgPSByZXF1aXJlKCdodHRwJyk7XG5jb25zdCBodHRwcyA9IHJlcXVpcmUoJ2h0dHBzJyk7XG5jb25zdCB6bGliID0gcmVxdWlyZSgnemxpYicpO1xuY29uc3QgU3RyZWFtID0gcmVxdWlyZSgnc3RyZWFtJyk7XG5jb25zdCBkYXRhVXJpVG9CdWZmZXIgPSByZXF1aXJlKCdkYXRhLXVyaS10by1idWZmZXInKTtcbmNvbnN0IHV0aWwgPSByZXF1aXJlKCd1dGlsJyk7XG5jb25zdCBCbG9iID0gcmVxdWlyZSgnZmV0Y2gtYmxvYicpO1xuY29uc3QgY3J5cHRvID0gcmVxdWlyZSgnY3J5cHRvJyk7XG5jb25zdCB1cmwgPSByZXF1aXJlKCd1cmwnKTtcblxuY2xhc3MgRmV0Y2hCYXNlRXJyb3IgZXh0ZW5kcyBFcnJvciB7XG5cdGNvbnN0cnVjdG9yKG1lc3NhZ2UsIHR5cGUpIHtcblx0XHRzdXBlcihtZXNzYWdlKTtcblx0XHQvLyBIaWRlIGN1c3RvbSBlcnJvciBpbXBsZW1lbnRhdGlvbiBkZXRhaWxzIGZyb20gZW5kLXVzZXJzXG5cdFx0RXJyb3IuY2FwdHVyZVN0YWNrVHJhY2UodGhpcywgdGhpcy5jb25zdHJ1Y3Rvcik7XG5cblx0XHR0aGlzLnR5cGUgPSB0eXBlO1xuXHR9XG5cblx0Z2V0IG5hbWUoKSB7XG5cdFx0cmV0dXJuIHRoaXMuY29uc3RydWN0b3IubmFtZTtcblx0fVxuXG5cdGdldCBbU3ltYm9sLnRvU3RyaW5nVGFnXSgpIHtcblx0XHRyZXR1cm4gdGhpcy5jb25zdHJ1Y3Rvci5uYW1lO1xuXHR9XG59XG5cbi8qKlxuICogQHR5cGVkZWYge3sgYWRkcmVzcz86IHN0cmluZywgY29kZTogc3RyaW5nLCBkZXN0Pzogc3RyaW5nLCBlcnJubzogbnVtYmVyLCBpbmZvPzogb2JqZWN0LCBtZXNzYWdlOiBzdHJpbmcsIHBhdGg/OiBzdHJpbmcsIHBvcnQ/OiBudW1iZXIsIHN5c2NhbGw6IHN0cmluZ319IFN5c3RlbUVycm9yXG4qL1xuXG4vKipcbiAqIEZldGNoRXJyb3IgaW50ZXJmYWNlIGZvciBvcGVyYXRpb25hbCBlcnJvcnNcbiAqL1xuY2xhc3MgRmV0Y2hFcnJvciBleHRlbmRzIEZldGNoQmFzZUVycm9yIHtcblx0LyoqXG5cdCAqIEBwYXJhbSAge3N0cmluZ30gbWVzc2FnZSAtICAgICAgRXJyb3IgbWVzc2FnZSBmb3IgaHVtYW5cblx0ICogQHBhcmFtICB7c3RyaW5nfSBbdHlwZV0gLSAgICAgICAgRXJyb3IgdHlwZSBmb3IgbWFjaGluZVxuXHQgKiBAcGFyYW0gIHtTeXN0ZW1FcnJvcn0gW3N5c3RlbUVycm9yXSAtIEZvciBOb2RlLmpzIHN5c3RlbSBlcnJvclxuXHQgKi9cblx0Y29uc3RydWN0b3IobWVzc2FnZSwgdHlwZSwgc3lzdGVtRXJyb3IpIHtcblx0XHRzdXBlcihtZXNzYWdlLCB0eXBlKTtcblx0XHQvLyBXaGVuIGVyci50eXBlIGlzIGBzeXN0ZW1gLCBlcnIuZXJyb3JlZFN5c0NhbGwgY29udGFpbnMgc3lzdGVtIGVycm9yIGFuZCBlcnIuY29kZSBjb250YWlucyBzeXN0ZW0gZXJyb3IgY29kZVxuXHRcdGlmIChzeXN0ZW1FcnJvcikge1xuXHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLW11bHRpLWFzc2lnblxuXHRcdFx0dGhpcy5jb2RlID0gdGhpcy5lcnJubyA9IHN5c3RlbUVycm9yLmNvZGU7XG5cdFx0XHR0aGlzLmVycm9yZWRTeXNDYWxsID0gc3lzdGVtRXJyb3Iuc3lzY2FsbDtcblx0XHR9XG5cdH1cbn1cblxuLyoqXG4gKiBJcy5qc1xuICpcbiAqIE9iamVjdCB0eXBlIGNoZWNrcy5cbiAqL1xuXG5jb25zdCBOQU1FID0gU3ltYm9sLnRvU3RyaW5nVGFnO1xuXG4vKipcbiAqIENoZWNrIGlmIGBvYmpgIGlzIGEgVVJMU2VhcmNoUGFyYW1zIG9iamVjdFxuICogcmVmOiBodHRwczovL2dpdGh1Yi5jb20vbm9kZS1mZXRjaC9ub2RlLWZldGNoL2lzc3Vlcy8yOTYjaXNzdWVjb21tZW50LTMwNzU5ODE0M1xuICpcbiAqIEBwYXJhbSAgeyp9IG9ialxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuY29uc3QgaXNVUkxTZWFyY2hQYXJhbWV0ZXJzID0gb2JqZWN0ID0+IHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuYXBwZW5kID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5kZWxldGUgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmdldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZ2V0QWxsID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5oYXMgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnNldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3Quc29ydCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdG9iamVjdFtOQU1FXSA9PT0gJ1VSTFNlYXJjaFBhcmFtcydcblx0KTtcbn07XG5cbi8qKlxuICogQ2hlY2sgaWYgYG9iamVjdGAgaXMgYSBXM0MgYEJsb2JgIG9iamVjdCAod2hpY2ggYEZpbGVgIGluaGVyaXRzIGZyb20pXG4gKlxuICogQHBhcmFtICB7Kn0gb2JqXG4gKiBAcmV0dXJuIHtib29sZWFufVxuICovXG5jb25zdCBpc0Jsb2IgPSBvYmplY3QgPT4ge1xuXHRyZXR1cm4gKFxuXHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0dHlwZW9mIG9iamVjdC5hcnJheUJ1ZmZlciA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QudHlwZSA9PT0gJ3N0cmluZycgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnN0cmVhbSA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHQvXihCbG9ifEZpbGUpJC8udGVzdChvYmplY3RbTkFNRV0pXG5cdCk7XG59O1xuXG4vKipcbiAqIENoZWNrIGlmIGBvYmpgIGlzIGEgc3BlYy1jb21wbGlhbnQgYEZvcm1EYXRhYCBvYmplY3RcbiAqXG4gKiBAcGFyYW0geyp9IG9iamVjdFxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuZnVuY3Rpb24gaXNGb3JtRGF0YShvYmplY3QpIHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuYXBwZW5kID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5zZXQgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmdldCA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZ2V0QWxsID09PSAnZnVuY3Rpb24nICYmXG5cdFx0dHlwZW9mIG9iamVjdC5kZWxldGUgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LmtleXMgPT09ICdmdW5jdGlvbicgJiZcblx0XHR0eXBlb2Ygb2JqZWN0LnZhbHVlcyA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuZW50cmllcyA9PT0gJ2Z1bmN0aW9uJyAmJlxuXHRcdHR5cGVvZiBvYmplY3QuY29uc3RydWN0b3IgPT09ICdmdW5jdGlvbicgJiZcblx0XHRvYmplY3RbTkFNRV0gPT09ICdGb3JtRGF0YSdcblx0KTtcbn1cblxuLyoqXG4gKiBDaGVjayBpZiBgb2JqYCBpcyBhbiBpbnN0YW5jZSBvZiBBYm9ydFNpZ25hbC5cbiAqXG4gKiBAcGFyYW0gIHsqfSBvYmpcbiAqIEByZXR1cm4ge2Jvb2xlYW59XG4gKi9cbmNvbnN0IGlzQWJvcnRTaWduYWwgPSBvYmplY3QgPT4ge1xuXHRyZXR1cm4gKFxuXHRcdHR5cGVvZiBvYmplY3QgPT09ICdvYmplY3QnICYmXG5cdFx0b2JqZWN0W05BTUVdID09PSAnQWJvcnRTaWduYWwnXG5cdCk7XG59O1xuXG5jb25zdCBjYXJyaWFnZSA9ICdcXHJcXG4nO1xuY29uc3QgZGFzaGVzID0gJy0nLnJlcGVhdCgyKTtcbmNvbnN0IGNhcnJpYWdlTGVuZ3RoID0gQnVmZmVyLmJ5dGVMZW5ndGgoY2FycmlhZ2UpO1xuXG4vKipcbiAqIEBwYXJhbSB7c3RyaW5nfSBib3VuZGFyeVxuICovXG5jb25zdCBnZXRGb290ZXIgPSBib3VuZGFyeSA9PiBgJHtkYXNoZXN9JHtib3VuZGFyeX0ke2Rhc2hlc30ke2NhcnJpYWdlLnJlcGVhdCgyKX1gO1xuXG4vKipcbiAqIEBwYXJhbSB7c3RyaW5nfSBib3VuZGFyeVxuICogQHBhcmFtIHtzdHJpbmd9IG5hbWVcbiAqIEBwYXJhbSB7Kn0gZmllbGRcbiAqXG4gKiBAcmV0dXJuIHtzdHJpbmd9XG4gKi9cbmZ1bmN0aW9uIGdldEhlYWRlcihib3VuZGFyeSwgbmFtZSwgZmllbGQpIHtcblx0bGV0IGhlYWRlciA9ICcnO1xuXG5cdGhlYWRlciArPSBgJHtkYXNoZXN9JHtib3VuZGFyeX0ke2NhcnJpYWdlfWA7XG5cdGhlYWRlciArPSBgQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPVwiJHtuYW1lfVwiYDtcblxuXHRpZiAoaXNCbG9iKGZpZWxkKSkge1xuXHRcdGhlYWRlciArPSBgOyBmaWxlbmFtZT1cIiR7ZmllbGQubmFtZX1cIiR7Y2FycmlhZ2V9YDtcblx0XHRoZWFkZXIgKz0gYENvbnRlbnQtVHlwZTogJHtmaWVsZC50eXBlIHx8ICdhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0nfWA7XG5cdH1cblxuXHRyZXR1cm4gYCR7aGVhZGVyfSR7Y2FycmlhZ2UucmVwZWF0KDIpfWA7XG59XG5cbi8qKlxuICogQHJldHVybiB7c3RyaW5nfVxuICovXG5jb25zdCBnZXRCb3VuZGFyeSA9ICgpID0+IGNyeXB0by5yYW5kb21CeXRlcyg4KS50b1N0cmluZygnaGV4Jyk7XG5cbi8qKlxuICogQHBhcmFtIHtGb3JtRGF0YX0gZm9ybVxuICogQHBhcmFtIHtzdHJpbmd9IGJvdW5kYXJ5XG4gKi9cbmFzeW5jIGZ1bmN0aW9uICogZm9ybURhdGFJdGVyYXRvcihmb3JtLCBib3VuZGFyeSkge1xuXHRmb3IgKGNvbnN0IFtuYW1lLCB2YWx1ZV0gb2YgZm9ybSkge1xuXHRcdHlpZWxkIGdldEhlYWRlcihib3VuZGFyeSwgbmFtZSwgdmFsdWUpO1xuXG5cdFx0aWYgKGlzQmxvYih2YWx1ZSkpIHtcblx0XHRcdHlpZWxkICogdmFsdWUuc3RyZWFtKCk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdHlpZWxkIHZhbHVlO1xuXHRcdH1cblxuXHRcdHlpZWxkIGNhcnJpYWdlO1xuXHR9XG5cblx0eWllbGQgZ2V0Rm9vdGVyKGJvdW5kYXJ5KTtcbn1cblxuLyoqXG4gKiBAcGFyYW0ge0Zvcm1EYXRhfSBmb3JtXG4gKiBAcGFyYW0ge3N0cmluZ30gYm91bmRhcnlcbiAqL1xuZnVuY3Rpb24gZ2V0Rm9ybURhdGFMZW5ndGgoZm9ybSwgYm91bmRhcnkpIHtcblx0bGV0IGxlbmd0aCA9IDA7XG5cblx0Zm9yIChjb25zdCBbbmFtZSwgdmFsdWVdIG9mIGZvcm0pIHtcblx0XHRsZW5ndGggKz0gQnVmZmVyLmJ5dGVMZW5ndGgoZ2V0SGVhZGVyKGJvdW5kYXJ5LCBuYW1lLCB2YWx1ZSkpO1xuXG5cdFx0aWYgKGlzQmxvYih2YWx1ZSkpIHtcblx0XHRcdGxlbmd0aCArPSB2YWx1ZS5zaXplO1xuXHRcdH0gZWxzZSB7XG5cdFx0XHRsZW5ndGggKz0gQnVmZmVyLmJ5dGVMZW5ndGgoU3RyaW5nKHZhbHVlKSk7XG5cdFx0fVxuXG5cdFx0bGVuZ3RoICs9IGNhcnJpYWdlTGVuZ3RoO1xuXHR9XG5cblx0bGVuZ3RoICs9IEJ1ZmZlci5ieXRlTGVuZ3RoKGdldEZvb3Rlcihib3VuZGFyeSkpO1xuXG5cdHJldHVybiBsZW5ndGg7XG59XG5cbmNvbnN0IElOVEVSTkFMUyA9IFN5bWJvbCgnQm9keSBpbnRlcm5hbHMnKTtcblxuLyoqXG4gKiBCb2R5IG1peGluXG4gKlxuICogUmVmOiBodHRwczovL2ZldGNoLnNwZWMud2hhdHdnLm9yZy8jYm9keVxuICpcbiAqIEBwYXJhbSAgIFN0cmVhbSAgYm9keSAgUmVhZGFibGUgc3RyZWFtXG4gKiBAcGFyYW0gICBPYmplY3QgIG9wdHMgIFJlc3BvbnNlIG9wdGlvbnNcbiAqIEByZXR1cm4gIFZvaWRcbiAqL1xuY2xhc3MgQm9keSB7XG5cdGNvbnN0cnVjdG9yKGJvZHksIHtcblx0XHRzaXplID0gMFxuXHR9ID0ge30pIHtcblx0XHRsZXQgYm91bmRhcnkgPSBudWxsO1xuXG5cdFx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHRcdC8vIEJvZHkgaXMgdW5kZWZpbmVkIG9yIG51bGxcblx0XHRcdGJvZHkgPSBudWxsO1xuXHRcdH0gZWxzZSBpZiAoaXNVUkxTZWFyY2hQYXJhbWV0ZXJzKGJvZHkpKSB7XG5cdFx0Ly8gQm9keSBpcyBhIFVSTFNlYXJjaFBhcmFtc1xuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkudG9TdHJpbmcoKSk7XG5cdFx0fSBlbHNlIGlmIChpc0Jsb2IoYm9keSkpIDsgZWxzZSBpZiAoQnVmZmVyLmlzQnVmZmVyKGJvZHkpKSA7IGVsc2UgaWYgKHV0aWwudHlwZXMuaXNBbnlBcnJheUJ1ZmZlcihib2R5KSkge1xuXHRcdFx0Ly8gQm9keSBpcyBBcnJheUJ1ZmZlclxuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkpO1xuXHRcdH0gZWxzZSBpZiAoQXJyYXlCdWZmZXIuaXNWaWV3KGJvZHkpKSB7XG5cdFx0XHQvLyBCb2R5IGlzIEFycmF5QnVmZmVyVmlld1xuXHRcdFx0Ym9keSA9IEJ1ZmZlci5mcm9tKGJvZHkuYnVmZmVyLCBib2R5LmJ5dGVPZmZzZXQsIGJvZHkuYnl0ZUxlbmd0aCk7XG5cdFx0fSBlbHNlIGlmIChib2R5IGluc3RhbmNlb2YgU3RyZWFtKSA7IGVsc2UgaWYgKGlzRm9ybURhdGEoYm9keSkpIHtcblx0XHRcdC8vIEJvZHkgaXMgYW4gaW5zdGFuY2Ugb2YgZm9ybWRhdGEtbm9kZVxuXHRcdFx0Ym91bmRhcnkgPSBgTm9kZUZldGNoRm9ybURhdGFCb3VuZGFyeSR7Z2V0Qm91bmRhcnkoKX1gO1xuXHRcdFx0Ym9keSA9IFN0cmVhbS5SZWFkYWJsZS5mcm9tKGZvcm1EYXRhSXRlcmF0b3IoYm9keSwgYm91bmRhcnkpKTtcblx0XHR9IGVsc2Uge1xuXHRcdFx0Ly8gTm9uZSBvZiB0aGUgYWJvdmVcblx0XHRcdC8vIGNvZXJjZSB0byBzdHJpbmcgdGhlbiBidWZmZXJcblx0XHRcdGJvZHkgPSBCdWZmZXIuZnJvbShTdHJpbmcoYm9keSkpO1xuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTXSA9IHtcblx0XHRcdGJvZHksXG5cdFx0XHRib3VuZGFyeSxcblx0XHRcdGRpc3R1cmJlZDogZmFsc2UsXG5cdFx0XHRlcnJvcjogbnVsbFxuXHRcdH07XG5cdFx0dGhpcy5zaXplID0gc2l6ZTtcblxuXHRcdGlmIChib2R5IGluc3RhbmNlb2YgU3RyZWFtKSB7XG5cdFx0XHRib2R5Lm9uKCdlcnJvcicsIGVyciA9PiB7XG5cdFx0XHRcdGNvbnN0IGVycm9yID0gZXJyIGluc3RhbmNlb2YgRmV0Y2hCYXNlRXJyb3IgP1xuXHRcdFx0XHRcdGVyciA6XG5cdFx0XHRcdFx0bmV3IEZldGNoRXJyb3IoYEludmFsaWQgcmVzcG9uc2UgYm9keSB3aGlsZSB0cnlpbmcgdG8gZmV0Y2ggJHt0aGlzLnVybH06ICR7ZXJyLm1lc3NhZ2V9YCwgJ3N5c3RlbScsIGVycik7XG5cdFx0XHRcdHRoaXNbSU5URVJOQUxTXS5lcnJvciA9IGVycm9yO1xuXHRcdFx0fSk7XG5cdFx0fVxuXHR9XG5cblx0Z2V0IGJvZHkoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTXS5ib2R5O1xuXHR9XG5cblx0Z2V0IGJvZHlVc2VkKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMU10uZGlzdHVyYmVkO1xuXHR9XG5cblx0LyoqXG5cdCAqIERlY29kZSByZXNwb25zZSBhcyBBcnJheUJ1ZmZlclxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyBhcnJheUJ1ZmZlcigpIHtcblx0XHRjb25zdCB7YnVmZmVyLCBieXRlT2Zmc2V0LCBieXRlTGVuZ3RofSA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBidWZmZXIuc2xpY2UoYnl0ZU9mZnNldCwgYnl0ZU9mZnNldCArIGJ5dGVMZW5ndGgpO1xuXHR9XG5cblx0LyoqXG5cdCAqIFJldHVybiByYXcgcmVzcG9uc2UgYXMgQmxvYlxuXHQgKlxuXHQgKiBAcmV0dXJuIFByb21pc2Vcblx0ICovXG5cdGFzeW5jIGJsb2IoKSB7XG5cdFx0Y29uc3QgY3QgPSAodGhpcy5oZWFkZXJzICYmIHRoaXMuaGVhZGVycy5nZXQoJ2NvbnRlbnQtdHlwZScpKSB8fCAodGhpc1tJTlRFUk5BTFNdLmJvZHkgJiYgdGhpc1tJTlRFUk5BTFNdLmJvZHkudHlwZSkgfHwgJyc7XG5cdFx0Y29uc3QgYnVmID0gYXdhaXQgdGhpcy5idWZmZXIoKTtcblxuXHRcdHJldHVybiBuZXcgQmxvYihbYnVmXSwge1xuXHRcdFx0dHlwZTogY3Rcblx0XHR9KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMganNvblxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyBqc29uKCkge1xuXHRcdGNvbnN0IGJ1ZmZlciA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBKU09OLnBhcnNlKGJ1ZmZlci50b1N0cmluZygpKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMgdGV4dFxuXHQgKlxuXHQgKiBAcmV0dXJuICBQcm9taXNlXG5cdCAqL1xuXHRhc3luYyB0ZXh0KCkge1xuXHRcdGNvbnN0IGJ1ZmZlciA9IGF3YWl0IGNvbnN1bWVCb2R5KHRoaXMpO1xuXHRcdHJldHVybiBidWZmZXIudG9TdHJpbmcoKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBEZWNvZGUgcmVzcG9uc2UgYXMgYnVmZmVyIChub24tc3BlYyBhcGkpXG5cdCAqXG5cdCAqIEByZXR1cm4gIFByb21pc2Vcblx0ICovXG5cdGJ1ZmZlcigpIHtcblx0XHRyZXR1cm4gY29uc3VtZUJvZHkodGhpcyk7XG5cdH1cbn1cblxuLy8gSW4gYnJvd3NlcnMsIGFsbCBwcm9wZXJ0aWVzIGFyZSBlbnVtZXJhYmxlLlxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoQm9keS5wcm90b3R5cGUsIHtcblx0Ym9keToge2VudW1lcmFibGU6IHRydWV9LFxuXHRib2R5VXNlZDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRhcnJheUJ1ZmZlcjoge2VudW1lcmFibGU6IHRydWV9LFxuXHRibG9iOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGpzb246IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0dGV4dDoge2VudW1lcmFibGU6IHRydWV9XG59KTtcblxuLyoqXG4gKiBDb25zdW1lIGFuZCBjb252ZXJ0IGFuIGVudGlyZSBCb2R5IHRvIGEgQnVmZmVyLlxuICpcbiAqIFJlZjogaHR0cHM6Ly9mZXRjaC5zcGVjLndoYXR3Zy5vcmcvI2NvbmNlcHQtYm9keS1jb25zdW1lLWJvZHlcbiAqXG4gKiBAcmV0dXJuIFByb21pc2VcbiAqL1xuYXN5bmMgZnVuY3Rpb24gY29uc3VtZUJvZHkoZGF0YSkge1xuXHRpZiAoZGF0YVtJTlRFUk5BTFNdLmRpc3R1cmJlZCkge1xuXHRcdHRocm93IG5ldyBUeXBlRXJyb3IoYGJvZHkgdXNlZCBhbHJlYWR5IGZvcjogJHtkYXRhLnVybH1gKTtcblx0fVxuXG5cdGRhdGFbSU5URVJOQUxTXS5kaXN0dXJiZWQgPSB0cnVlO1xuXG5cdGlmIChkYXRhW0lOVEVSTkFMU10uZXJyb3IpIHtcblx0XHR0aHJvdyBkYXRhW0lOVEVSTkFMU10uZXJyb3I7XG5cdH1cblxuXHRsZXQge2JvZHl9ID0gZGF0YTtcblxuXHQvLyBCb2R5IGlzIG51bGxcblx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmFsbG9jKDApO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBibG9iXG5cdGlmIChpc0Jsb2IoYm9keSkpIHtcblx0XHRib2R5ID0gYm9keS5zdHJlYW0oKTtcblx0fVxuXG5cdC8vIEJvZHkgaXMgYnVmZmVyXG5cdGlmIChCdWZmZXIuaXNCdWZmZXIoYm9keSkpIHtcblx0XHRyZXR1cm4gYm9keTtcblx0fVxuXG5cdC8qIGM4IGlnbm9yZSBuZXh0IDMgKi9cblx0aWYgKCEoYm9keSBpbnN0YW5jZW9mIFN0cmVhbSkpIHtcblx0XHRyZXR1cm4gQnVmZmVyLmFsbG9jKDApO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBzdHJlYW1cblx0Ly8gZ2V0IHJlYWR5IHRvIGFjdHVhbGx5IGNvbnN1bWUgdGhlIGJvZHlcblx0Y29uc3QgYWNjdW0gPSBbXTtcblx0bGV0IGFjY3VtQnl0ZXMgPSAwO1xuXG5cdHRyeSB7XG5cdFx0Zm9yIGF3YWl0IChjb25zdCBjaHVuayBvZiBib2R5KSB7XG5cdFx0XHRpZiAoZGF0YS5zaXplID4gMCAmJiBhY2N1bUJ5dGVzICsgY2h1bmsubGVuZ3RoID4gZGF0YS5zaXplKSB7XG5cdFx0XHRcdGNvbnN0IGVyciA9IG5ldyBGZXRjaEVycm9yKGBjb250ZW50IHNpemUgYXQgJHtkYXRhLnVybH0gb3ZlciBsaW1pdDogJHtkYXRhLnNpemV9YCwgJ21heC1zaXplJyk7XG5cdFx0XHRcdGJvZHkuZGVzdHJveShlcnIpO1xuXHRcdFx0XHR0aHJvdyBlcnI7XG5cdFx0XHR9XG5cblx0XHRcdGFjY3VtQnl0ZXMgKz0gY2h1bmsubGVuZ3RoO1xuXHRcdFx0YWNjdW0ucHVzaChjaHVuayk7XG5cdFx0fVxuXHR9IGNhdGNoIChlcnJvcikge1xuXHRcdGlmIChlcnJvciBpbnN0YW5jZW9mIEZldGNoQmFzZUVycm9yKSB7XG5cdFx0XHR0aHJvdyBlcnJvcjtcblx0XHR9IGVsc2Uge1xuXHRcdFx0Ly8gT3RoZXIgZXJyb3JzLCBzdWNoIGFzIGluY29ycmVjdCBjb250ZW50LWVuY29kaW5nXG5cdFx0XHR0aHJvdyBuZXcgRmV0Y2hFcnJvcihgSW52YWxpZCByZXNwb25zZSBib2R5IHdoaWxlIHRyeWluZyB0byBmZXRjaCAke2RhdGEudXJsfTogJHtlcnJvci5tZXNzYWdlfWAsICdzeXN0ZW0nLCBlcnJvcik7XG5cdFx0fVxuXHR9XG5cblx0aWYgKGJvZHkucmVhZGFibGVFbmRlZCA9PT0gdHJ1ZSB8fCBib2R5Ll9yZWFkYWJsZVN0YXRlLmVuZGVkID09PSB0cnVlKSB7XG5cdFx0dHJ5IHtcblx0XHRcdGlmIChhY2N1bS5ldmVyeShjID0+IHR5cGVvZiBjID09PSAnc3RyaW5nJykpIHtcblx0XHRcdFx0cmV0dXJuIEJ1ZmZlci5mcm9tKGFjY3VtLmpvaW4oJycpKTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIEJ1ZmZlci5jb25jYXQoYWNjdW0sIGFjY3VtQnl0ZXMpO1xuXHRcdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0XHR0aHJvdyBuZXcgRmV0Y2hFcnJvcihgQ291bGQgbm90IGNyZWF0ZSBCdWZmZXIgZnJvbSByZXNwb25zZSBib2R5IGZvciAke2RhdGEudXJsfTogJHtlcnJvci5tZXNzYWdlfWAsICdzeXN0ZW0nLCBlcnJvcik7XG5cdFx0fVxuXHR9IGVsc2Uge1xuXHRcdHRocm93IG5ldyBGZXRjaEVycm9yKGBQcmVtYXR1cmUgY2xvc2Ugb2Ygc2VydmVyIHJlc3BvbnNlIHdoaWxlIHRyeWluZyB0byBmZXRjaCAke2RhdGEudXJsfWApO1xuXHR9XG59XG5cbi8qKlxuICogQ2xvbmUgYm9keSBnaXZlbiBSZXMvUmVxIGluc3RhbmNlXG4gKlxuICogQHBhcmFtICAgTWl4ZWQgICBpbnN0YW5jZSAgICAgICBSZXNwb25zZSBvciBSZXF1ZXN0IGluc3RhbmNlXG4gKiBAcGFyYW0gICBTdHJpbmcgIGhpZ2hXYXRlck1hcmsgIGhpZ2hXYXRlck1hcmsgZm9yIGJvdGggUGFzc1Rocm91Z2ggYm9keSBzdHJlYW1zXG4gKiBAcmV0dXJuICBNaXhlZFxuICovXG5jb25zdCBjbG9uZSA9IChpbnN0YW5jZSwgaGlnaFdhdGVyTWFyaykgPT4ge1xuXHRsZXQgcDE7XG5cdGxldCBwMjtcblx0bGV0IHtib2R5fSA9IGluc3RhbmNlO1xuXG5cdC8vIERvbid0IGFsbG93IGNsb25pbmcgYSB1c2VkIGJvZHlcblx0aWYgKGluc3RhbmNlLmJvZHlVc2VkKSB7XG5cdFx0dGhyb3cgbmV3IEVycm9yKCdjYW5ub3QgY2xvbmUgYm9keSBhZnRlciBpdCBpcyB1c2VkJyk7XG5cdH1cblxuXHQvLyBDaGVjayB0aGF0IGJvZHkgaXMgYSBzdHJlYW0gYW5kIG5vdCBmb3JtLWRhdGEgb2JqZWN0XG5cdC8vIG5vdGU6IHdlIGNhbid0IGNsb25lIHRoZSBmb3JtLWRhdGEgb2JqZWN0IHdpdGhvdXQgaGF2aW5nIGl0IGFzIGEgZGVwZW5kZW5jeVxuXHRpZiAoKGJvZHkgaW5zdGFuY2VvZiBTdHJlYW0pICYmICh0eXBlb2YgYm9keS5nZXRCb3VuZGFyeSAhPT0gJ2Z1bmN0aW9uJykpIHtcblx0XHQvLyBUZWUgaW5zdGFuY2UgYm9keVxuXHRcdHAxID0gbmV3IFN0cmVhbS5QYXNzVGhyb3VnaCh7aGlnaFdhdGVyTWFya30pO1xuXHRcdHAyID0gbmV3IFN0cmVhbS5QYXNzVGhyb3VnaCh7aGlnaFdhdGVyTWFya30pO1xuXHRcdGJvZHkucGlwZShwMSk7XG5cdFx0Ym9keS5waXBlKHAyKTtcblx0XHQvLyBTZXQgaW5zdGFuY2UgYm9keSB0byB0ZWVkIGJvZHkgYW5kIHJldHVybiB0aGUgb3RoZXIgdGVlZCBib2R5XG5cdFx0aW5zdGFuY2VbSU5URVJOQUxTXS5ib2R5ID0gcDE7XG5cdFx0Ym9keSA9IHAyO1xuXHR9XG5cblx0cmV0dXJuIGJvZHk7XG59O1xuXG4vKipcbiAqIFBlcmZvcm1zIHRoZSBvcGVyYXRpb24gXCJleHRyYWN0IGEgYENvbnRlbnQtVHlwZWAgdmFsdWUgZnJvbSB8b2JqZWN0fFwiIGFzXG4gKiBzcGVjaWZpZWQgaW4gdGhlIHNwZWNpZmljYXRpb246XG4gKiBodHRwczovL2ZldGNoLnNwZWMud2hhdHdnLm9yZy8jY29uY2VwdC1ib2R5aW5pdC1leHRyYWN0XG4gKlxuICogVGhpcyBmdW5jdGlvbiBhc3N1bWVzIHRoYXQgaW5zdGFuY2UuYm9keSBpcyBwcmVzZW50LlxuICpcbiAqIEBwYXJhbSB7YW55fSBib2R5IEFueSBvcHRpb25zLmJvZHkgaW5wdXRcbiAqIEByZXR1cm5zIHtzdHJpbmcgfCBudWxsfVxuICovXG5jb25zdCBleHRyYWN0Q29udGVudFR5cGUgPSAoYm9keSwgcmVxdWVzdCkgPT4ge1xuXHQvLyBCb2R5IGlzIG51bGwgb3IgdW5kZWZpbmVkXG5cdGlmIChib2R5ID09PSBudWxsKSB7XG5cdFx0cmV0dXJuIG51bGw7XG5cdH1cblxuXHQvLyBCb2R5IGlzIHN0cmluZ1xuXHRpZiAodHlwZW9mIGJvZHkgPT09ICdzdHJpbmcnKSB7XG5cdFx0cmV0dXJuICd0ZXh0L3BsYWluO2NoYXJzZXQ9VVRGLTgnO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBhIFVSTFNlYXJjaFBhcmFtc1xuXHRpZiAoaXNVUkxTZWFyY2hQYXJhbWV0ZXJzKGJvZHkpKSB7XG5cdFx0cmV0dXJuICdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQ7Y2hhcnNldD1VVEYtOCc7XG5cdH1cblxuXHQvLyBCb2R5IGlzIGJsb2Jcblx0aWYgKGlzQmxvYihib2R5KSkge1xuXHRcdHJldHVybiBib2R5LnR5cGUgfHwgbnVsbDtcblx0fVxuXG5cdC8vIEJvZHkgaXMgYSBCdWZmZXIgKEJ1ZmZlciwgQXJyYXlCdWZmZXIgb3IgQXJyYXlCdWZmZXJWaWV3KVxuXHRpZiAoQnVmZmVyLmlzQnVmZmVyKGJvZHkpIHx8IHV0aWwudHlwZXMuaXNBbnlBcnJheUJ1ZmZlcihib2R5KSB8fCBBcnJheUJ1ZmZlci5pc1ZpZXcoYm9keSkpIHtcblx0XHRyZXR1cm4gbnVsbDtcblx0fVxuXG5cdC8vIERldGVjdCBmb3JtIGRhdGEgaW5wdXQgZnJvbSBmb3JtLWRhdGEgbW9kdWxlXG5cdGlmIChib2R5ICYmIHR5cGVvZiBib2R5LmdldEJvdW5kYXJ5ID09PSAnZnVuY3Rpb24nKSB7XG5cdFx0cmV0dXJuIGBtdWx0aXBhcnQvZm9ybS1kYXRhO2JvdW5kYXJ5PSR7Ym9keS5nZXRCb3VuZGFyeSgpfWA7XG5cdH1cblxuXHRpZiAoaXNGb3JtRGF0YShib2R5KSkge1xuXHRcdHJldHVybiBgbXVsdGlwYXJ0L2Zvcm0tZGF0YTsgYm91bmRhcnk9JHtyZXF1ZXN0W0lOVEVSTkFMU10uYm91bmRhcnl9YDtcblx0fVxuXG5cdC8vIEJvZHkgaXMgc3RyZWFtIC0gY2FuJ3QgcmVhbGx5IGRvIG11Y2ggYWJvdXQgdGhpc1xuXHRpZiAoYm9keSBpbnN0YW5jZW9mIFN0cmVhbSkge1xuXHRcdHJldHVybiBudWxsO1xuXHR9XG5cblx0Ly8gQm9keSBjb25zdHJ1Y3RvciBkZWZhdWx0cyBvdGhlciB0aGluZ3MgdG8gc3RyaW5nXG5cdHJldHVybiAndGV4dC9wbGFpbjtjaGFyc2V0PVVURi04Jztcbn07XG5cbi8qKlxuICogVGhlIEZldGNoIFN0YW5kYXJkIHRyZWF0cyB0aGlzIGFzIGlmIFwidG90YWwgYnl0ZXNcIiBpcyBhIHByb3BlcnR5IG9uIHRoZSBib2R5LlxuICogRm9yIHVzLCB3ZSBoYXZlIHRvIGV4cGxpY2l0bHkgZ2V0IGl0IHdpdGggYSBmdW5jdGlvbi5cbiAqXG4gKiByZWY6IGh0dHBzOi8vZmV0Y2guc3BlYy53aGF0d2cub3JnLyNjb25jZXB0LWJvZHktdG90YWwtYnl0ZXNcbiAqXG4gKiBAcGFyYW0ge2FueX0gb2JqLmJvZHkgQm9keSBvYmplY3QgZnJvbSB0aGUgQm9keSBpbnN0YW5jZS5cbiAqIEByZXR1cm5zIHtudW1iZXIgfCBudWxsfVxuICovXG5jb25zdCBnZXRUb3RhbEJ5dGVzID0gcmVxdWVzdCA9PiB7XG5cdGNvbnN0IHtib2R5fSA9IHJlcXVlc3Q7XG5cblx0Ly8gQm9keSBpcyBudWxsIG9yIHVuZGVmaW5lZFxuXHRpZiAoYm9keSA9PT0gbnVsbCkge1xuXHRcdHJldHVybiAwO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBCbG9iXG5cdGlmIChpc0Jsb2IoYm9keSkpIHtcblx0XHRyZXR1cm4gYm9keS5zaXplO1xuXHR9XG5cblx0Ly8gQm9keSBpcyBCdWZmZXJcblx0aWYgKEJ1ZmZlci5pc0J1ZmZlcihib2R5KSkge1xuXHRcdHJldHVybiBib2R5Lmxlbmd0aDtcblx0fVxuXG5cdC8vIERldGVjdCBmb3JtIGRhdGEgaW5wdXQgZnJvbSBmb3JtLWRhdGEgbW9kdWxlXG5cdGlmIChib2R5ICYmIHR5cGVvZiBib2R5LmdldExlbmd0aFN5bmMgPT09ICdmdW5jdGlvbicpIHtcblx0XHRyZXR1cm4gYm9keS5oYXNLbm93bkxlbmd0aCAmJiBib2R5Lmhhc0tub3duTGVuZ3RoKCkgPyBib2R5LmdldExlbmd0aFN5bmMoKSA6IG51bGw7XG5cdH1cblxuXHQvLyBCb2R5IGlzIGEgc3BlYy1jb21wbGlhbnQgZm9ybS1kYXRhXG5cdGlmIChpc0Zvcm1EYXRhKGJvZHkpKSB7XG5cdFx0cmV0dXJuIGdldEZvcm1EYXRhTGVuZ3RoKHJlcXVlc3RbSU5URVJOQUxTXS5ib3VuZGFyeSk7XG5cdH1cblxuXHQvLyBCb2R5IGlzIHN0cmVhbVxuXHRyZXR1cm4gbnVsbDtcbn07XG5cbi8qKlxuICogV3JpdGUgYSBCb2R5IHRvIGEgTm9kZS5qcyBXcml0YWJsZVN0cmVhbSAoZS5nLiBodHRwLlJlcXVlc3QpIG9iamVjdC5cbiAqXG4gKiBAcGFyYW0ge1N0cmVhbS5Xcml0YWJsZX0gZGVzdCBUaGUgc3RyZWFtIHRvIHdyaXRlIHRvLlxuICogQHBhcmFtIG9iai5ib2R5IEJvZHkgb2JqZWN0IGZyb20gdGhlIEJvZHkgaW5zdGFuY2UuXG4gKiBAcmV0dXJucyB7dm9pZH1cbiAqL1xuY29uc3Qgd3JpdGVUb1N0cmVhbSA9IChkZXN0LCB7Ym9keX0pID0+IHtcblx0aWYgKGJvZHkgPT09IG51bGwpIHtcblx0XHQvLyBCb2R5IGlzIG51bGxcblx0XHRkZXN0LmVuZCgpO1xuXHR9IGVsc2UgaWYgKGlzQmxvYihib2R5KSkge1xuXHRcdC8vIEJvZHkgaXMgQmxvYlxuXHRcdGJvZHkuc3RyZWFtKCkucGlwZShkZXN0KTtcblx0fSBlbHNlIGlmIChCdWZmZXIuaXNCdWZmZXIoYm9keSkpIHtcblx0XHQvLyBCb2R5IGlzIGJ1ZmZlclxuXHRcdGRlc3Qud3JpdGUoYm9keSk7XG5cdFx0ZGVzdC5lbmQoKTtcblx0fSBlbHNlIHtcblx0XHQvLyBCb2R5IGlzIHN0cmVhbVxuXHRcdGJvZHkucGlwZShkZXN0KTtcblx0fVxufTtcblxuLyoqXG4gKiBIZWFkZXJzLmpzXG4gKlxuICogSGVhZGVycyBjbGFzcyBvZmZlcnMgY29udmVuaWVudCBoZWxwZXJzXG4gKi9cblxuY29uc3QgdmFsaWRhdGVIZWFkZXJOYW1lID0gdHlwZW9mIGh0dHAudmFsaWRhdGVIZWFkZXJOYW1lID09PSAnZnVuY3Rpb24nID9cblx0aHR0cC52YWxpZGF0ZUhlYWRlck5hbWUgOlxuXHRuYW1lID0+IHtcblx0XHRpZiAoIS9eW1xcXmBcXC1cXHchIyQlJicqKy58fl0rJC8udGVzdChuYW1lKSkge1xuXHRcdFx0Y29uc3QgZXJyID0gbmV3IFR5cGVFcnJvcihgSGVhZGVyIG5hbWUgbXVzdCBiZSBhIHZhbGlkIEhUVFAgdG9rZW4gWyR7bmFtZX1dYCk7XG5cdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXJyLCAnY29kZScsIHt2YWx1ZTogJ0VSUl9JTlZBTElEX0hUVFBfVE9LRU4nfSk7XG5cdFx0XHR0aHJvdyBlcnI7XG5cdFx0fVxuXHR9O1xuXG5jb25zdCB2YWxpZGF0ZUhlYWRlclZhbHVlID0gdHlwZW9mIGh0dHAudmFsaWRhdGVIZWFkZXJWYWx1ZSA9PT0gJ2Z1bmN0aW9uJyA/XG5cdGh0dHAudmFsaWRhdGVIZWFkZXJWYWx1ZSA6XG5cdChuYW1lLCB2YWx1ZSkgPT4ge1xuXHRcdGlmICgvW15cXHRcXHUwMDIwLVxcdTAwN0VcXHUwMDgwLVxcdTAwRkZdLy50ZXN0KHZhbHVlKSkge1xuXHRcdFx0Y29uc3QgZXJyID0gbmV3IFR5cGVFcnJvcihgSW52YWxpZCBjaGFyYWN0ZXIgaW4gaGVhZGVyIGNvbnRlbnQgW1wiJHtuYW1lfVwiXWApO1xuXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGVyciwgJ2NvZGUnLCB7dmFsdWU6ICdFUlJfSU5WQUxJRF9DSEFSJ30pO1xuXHRcdFx0dGhyb3cgZXJyO1xuXHRcdH1cblx0fTtcblxuLyoqXG4gKiBAdHlwZWRlZiB7SGVhZGVycyB8IFJlY29yZDxzdHJpbmcsIHN0cmluZz4gfCBJdGVyYWJsZTxyZWFkb25seSBbc3RyaW5nLCBzdHJpbmddPiB8IEl0ZXJhYmxlPEl0ZXJhYmxlPHN0cmluZz4+fSBIZWFkZXJzSW5pdFxuICovXG5cbi8qKlxuICogVGhpcyBGZXRjaCBBUEkgaW50ZXJmYWNlIGFsbG93cyB5b3UgdG8gcGVyZm9ybSB2YXJpb3VzIGFjdGlvbnMgb24gSFRUUCByZXF1ZXN0IGFuZCByZXNwb25zZSBoZWFkZXJzLlxuICogVGhlc2UgYWN0aW9ucyBpbmNsdWRlIHJldHJpZXZpbmcsIHNldHRpbmcsIGFkZGluZyB0bywgYW5kIHJlbW92aW5nLlxuICogQSBIZWFkZXJzIG9iamVjdCBoYXMgYW4gYXNzb2NpYXRlZCBoZWFkZXIgbGlzdCwgd2hpY2ggaXMgaW5pdGlhbGx5IGVtcHR5IGFuZCBjb25zaXN0cyBvZiB6ZXJvIG9yIG1vcmUgbmFtZSBhbmQgdmFsdWUgcGFpcnMuXG4gKiBZb3UgY2FuIGFkZCB0byB0aGlzIHVzaW5nIG1ldGhvZHMgbGlrZSBhcHBlbmQoKSAoc2VlIEV4YW1wbGVzLilcbiAqIEluIGFsbCBtZXRob2RzIG9mIHRoaXMgaW50ZXJmYWNlLCBoZWFkZXIgbmFtZXMgYXJlIG1hdGNoZWQgYnkgY2FzZS1pbnNlbnNpdGl2ZSBieXRlIHNlcXVlbmNlLlxuICpcbiAqL1xuY2xhc3MgSGVhZGVycyBleHRlbmRzIFVSTFNlYXJjaFBhcmFtcyB7XG5cdC8qKlxuXHQgKiBIZWFkZXJzIGNsYXNzXG5cdCAqXG5cdCAqIEBjb25zdHJ1Y3RvclxuXHQgKiBAcGFyYW0ge0hlYWRlcnNJbml0fSBbaW5pdF0gLSBSZXNwb25zZSBoZWFkZXJzXG5cdCAqL1xuXHRjb25zdHJ1Y3Rvcihpbml0KSB7XG5cdFx0Ly8gVmFsaWRhdGUgYW5kIG5vcm1hbGl6ZSBpbml0IG9iamVjdCBpbiBbbmFtZSwgdmFsdWUocyldW11cblx0XHQvKiogQHR5cGUge3N0cmluZ1tdW119ICovXG5cdFx0bGV0IHJlc3VsdCA9IFtdO1xuXHRcdGlmIChpbml0IGluc3RhbmNlb2YgSGVhZGVycykge1xuXHRcdFx0Y29uc3QgcmF3ID0gaW5pdC5yYXcoKTtcblx0XHRcdGZvciAoY29uc3QgW25hbWUsIHZhbHVlc10gb2YgT2JqZWN0LmVudHJpZXMocmF3KSkge1xuXHRcdFx0XHRyZXN1bHQucHVzaCguLi52YWx1ZXMubWFwKHZhbHVlID0+IFtuYW1lLCB2YWx1ZV0pKTtcblx0XHRcdH1cblx0XHR9IGVsc2UgaWYgKGluaXQgPT0gbnVsbCkgOyBlbHNlIGlmICh0eXBlb2YgaW5pdCA9PT0gJ29iamVjdCcgJiYgIXV0aWwudHlwZXMuaXNCb3hlZFByaW1pdGl2ZShpbml0KSkge1xuXHRcdFx0Y29uc3QgbWV0aG9kID0gaW5pdFtTeW1ib2wuaXRlcmF0b3JdO1xuXHRcdFx0Ly8gZXNsaW50LWRpc2FibGUtbmV4dC1saW5lIG5vLWVxLW51bGwsIGVxZXFlcVxuXHRcdFx0aWYgKG1ldGhvZCA9PSBudWxsKSB7XG5cdFx0XHRcdC8vIFJlY29yZDxCeXRlU3RyaW5nLCBCeXRlU3RyaW5nPlxuXHRcdFx0XHRyZXN1bHQucHVzaCguLi5PYmplY3QuZW50cmllcyhpbml0KSk7XG5cdFx0XHR9IGVsc2Uge1xuXHRcdFx0XHRpZiAodHlwZW9mIG1ldGhvZCAhPT0gJ2Z1bmN0aW9uJykge1xuXHRcdFx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ0hlYWRlciBwYWlycyBtdXN0IGJlIGl0ZXJhYmxlJyk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHQvLyBTZXF1ZW5jZTxzZXF1ZW5jZTxCeXRlU3RyaW5nPj5cblx0XHRcdFx0Ly8gTm90ZTogcGVyIHNwZWMgd2UgaGF2ZSB0byBmaXJzdCBleGhhdXN0IHRoZSBsaXN0cyB0aGVuIHByb2Nlc3MgdGhlbVxuXHRcdFx0XHRyZXN1bHQgPSBbLi4uaW5pdF1cblx0XHRcdFx0XHQubWFwKHBhaXIgPT4ge1xuXHRcdFx0XHRcdFx0aWYgKFxuXHRcdFx0XHRcdFx0XHR0eXBlb2YgcGFpciAhPT0gJ29iamVjdCcgfHwgdXRpbC50eXBlcy5pc0JveGVkUHJpbWl0aXZlKHBhaXIpXG5cdFx0XHRcdFx0XHQpIHtcblx0XHRcdFx0XHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignRWFjaCBoZWFkZXIgcGFpciBtdXN0IGJlIGFuIGl0ZXJhYmxlIG9iamVjdCcpO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRyZXR1cm4gWy4uLnBhaXJdO1xuXHRcdFx0XHRcdH0pLm1hcChwYWlyID0+IHtcblx0XHRcdFx0XHRcdGlmIChwYWlyLmxlbmd0aCAhPT0gMikge1xuXHRcdFx0XHRcdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdFYWNoIGhlYWRlciBwYWlyIG11c3QgYmUgYSBuYW1lL3ZhbHVlIHR1cGxlJyk7XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdHJldHVybiBbLi4ucGFpcl07XG5cdFx0XHRcdFx0fSk7XG5cdFx0XHR9XG5cdFx0fSBlbHNlIHtcblx0XHRcdHRocm93IG5ldyBUeXBlRXJyb3IoJ0ZhaWxlZCB0byBjb25zdHJ1Y3QgXFwnSGVhZGVyc1xcJzogVGhlIHByb3ZpZGVkIHZhbHVlIGlzIG5vdCBvZiB0eXBlIFxcJyhzZXF1ZW5jZTxzZXF1ZW5jZTxCeXRlU3RyaW5nPj4gb3IgcmVjb3JkPEJ5dGVTdHJpbmcsIEJ5dGVTdHJpbmc+KScpO1xuXHRcdH1cblxuXHRcdC8vIFZhbGlkYXRlIGFuZCBsb3dlcmNhc2Vcblx0XHRyZXN1bHQgPVxuXHRcdFx0cmVzdWx0Lmxlbmd0aCA+IDAgP1xuXHRcdFx0XHRyZXN1bHQubWFwKChbbmFtZSwgdmFsdWVdKSA9PiB7XG5cdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJOYW1lKG5hbWUpO1xuXHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyVmFsdWUobmFtZSwgU3RyaW5nKHZhbHVlKSk7XG5cdFx0XHRcdFx0cmV0dXJuIFtTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKSwgU3RyaW5nKHZhbHVlKV07XG5cdFx0XHRcdH0pIDpcblx0XHRcdFx0dW5kZWZpbmVkO1xuXG5cdFx0c3VwZXIocmVzdWx0KTtcblxuXHRcdC8vIFJldHVybmluZyBhIFByb3h5IHRoYXQgd2lsbCBsb3dlcmNhc2Uga2V5IG5hbWVzLCB2YWxpZGF0ZSBwYXJhbWV0ZXJzIGFuZCBzb3J0IGtleXNcblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tY29uc3RydWN0b3ItcmV0dXJuXG5cdFx0cmV0dXJuIG5ldyBQcm94eSh0aGlzLCB7XG5cdFx0XHRnZXQodGFyZ2V0LCBwLCByZWNlaXZlcikge1xuXHRcdFx0XHRzd2l0Y2ggKHApIHtcblx0XHRcdFx0XHRjYXNlICdhcHBlbmQnOlxuXHRcdFx0XHRcdGNhc2UgJ3NldCc6XG5cdFx0XHRcdFx0XHRyZXR1cm4gKG5hbWUsIHZhbHVlKSA9PiB7XG5cdFx0XHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyTmFtZShuYW1lKTtcblx0XHRcdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJWYWx1ZShuYW1lLCBTdHJpbmcodmFsdWUpKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIFVSTFNlYXJjaFBhcmFtcy5wcm90b3R5cGVbcF0uY2FsbChcblx0XHRcdFx0XHRcdFx0XHRyZWNlaXZlcixcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKSxcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcodmFsdWUpXG5cdFx0XHRcdFx0XHRcdCk7XG5cdFx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0Y2FzZSAnZGVsZXRlJzpcblx0XHRcdFx0XHRjYXNlICdoYXMnOlxuXHRcdFx0XHRcdGNhc2UgJ2dldEFsbCc6XG5cdFx0XHRcdFx0XHRyZXR1cm4gbmFtZSA9PiB7XG5cdFx0XHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyTmFtZShuYW1lKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIFVSTFNlYXJjaFBhcmFtcy5wcm90b3R5cGVbcF0uY2FsbChcblx0XHRcdFx0XHRcdFx0XHRyZWNlaXZlcixcblx0XHRcdFx0XHRcdFx0XHRTdHJpbmcobmFtZSkudG9Mb3dlckNhc2UoKVxuXHRcdFx0XHRcdFx0XHQpO1xuXHRcdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdGNhc2UgJ2tleXMnOlxuXHRcdFx0XHRcdFx0cmV0dXJuICgpID0+IHtcblx0XHRcdFx0XHRcdFx0dGFyZ2V0LnNvcnQoKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuIG5ldyBTZXQoVVJMU2VhcmNoUGFyYW1zLnByb3RvdHlwZS5rZXlzLmNhbGwodGFyZ2V0KSkua2V5cygpO1xuXHRcdFx0XHRcdFx0fTtcblxuXHRcdFx0XHRcdGRlZmF1bHQ6XG5cdFx0XHRcdFx0XHRyZXR1cm4gUmVmbGVjdC5nZXQodGFyZ2V0LCBwLCByZWNlaXZlcik7XG5cdFx0XHRcdH1cblx0XHRcdH1cblx0XHRcdC8qIGM4IGlnbm9yZSBuZXh0ICovXG5cdFx0fSk7XG5cdH1cblxuXHRnZXQgW1N5bWJvbC50b1N0cmluZ1RhZ10oKSB7XG5cdFx0cmV0dXJuIHRoaXMuY29uc3RydWN0b3IubmFtZTtcblx0fVxuXG5cdHRvU3RyaW5nKCkge1xuXHRcdHJldHVybiBPYmplY3QucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwodGhpcyk7XG5cdH1cblxuXHRnZXQobmFtZSkge1xuXHRcdGNvbnN0IHZhbHVlcyA9IHRoaXMuZ2V0QWxsKG5hbWUpO1xuXHRcdGlmICh2YWx1ZXMubGVuZ3RoID09PSAwKSB7XG5cdFx0XHRyZXR1cm4gbnVsbDtcblx0XHR9XG5cblx0XHRsZXQgdmFsdWUgPSB2YWx1ZXMuam9pbignLCAnKTtcblx0XHRpZiAoL15jb250ZW50LWVuY29kaW5nJC9pLnRlc3QobmFtZSkpIHtcblx0XHRcdHZhbHVlID0gdmFsdWUudG9Mb3dlckNhc2UoKTtcblx0XHR9XG5cblx0XHRyZXR1cm4gdmFsdWU7XG5cdH1cblxuXHRmb3JFYWNoKGNhbGxiYWNrKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHRjYWxsYmFjayh0aGlzLmdldChuYW1lKSwgbmFtZSk7XG5cdFx0fVxuXHR9XG5cblx0KiB2YWx1ZXMoKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHR5aWVsZCB0aGlzLmdldChuYW1lKTtcblx0XHR9XG5cdH1cblxuXHQvKipcblx0ICogQHR5cGUgeygpID0+IEl0ZXJhYmxlSXRlcmF0b3I8W3N0cmluZywgc3RyaW5nXT59XG5cdCAqL1xuXHQqIGVudHJpZXMoKSB7XG5cdFx0Zm9yIChjb25zdCBuYW1lIG9mIHRoaXMua2V5cygpKSB7XG5cdFx0XHR5aWVsZCBbbmFtZSwgdGhpcy5nZXQobmFtZSldO1xuXHRcdH1cblx0fVxuXG5cdFtTeW1ib2wuaXRlcmF0b3JdKCkge1xuXHRcdHJldHVybiB0aGlzLmVudHJpZXMoKTtcblx0fVxuXG5cdC8qKlxuXHQgKiBOb2RlLWZldGNoIG5vbi1zcGVjIG1ldGhvZFxuXHQgKiByZXR1cm5pbmcgYWxsIGhlYWRlcnMgYW5kIHRoZWlyIHZhbHVlcyBhcyBhcnJheVxuXHQgKiBAcmV0dXJucyB7UmVjb3JkPHN0cmluZywgc3RyaW5nW10+fVxuXHQgKi9cblx0cmF3KCkge1xuXHRcdHJldHVybiBbLi4udGhpcy5rZXlzKCldLnJlZHVjZSgocmVzdWx0LCBrZXkpID0+IHtcblx0XHRcdHJlc3VsdFtrZXldID0gdGhpcy5nZXRBbGwoa2V5KTtcblx0XHRcdHJldHVybiByZXN1bHQ7XG5cdFx0fSwge30pO1xuXHR9XG5cblx0LyoqXG5cdCAqIEZvciBiZXR0ZXIgY29uc29sZS5sb2coaGVhZGVycykgYW5kIGFsc28gdG8gY29udmVydCBIZWFkZXJzIGludG8gTm9kZS5qcyBSZXF1ZXN0IGNvbXBhdGlibGUgZm9ybWF0XG5cdCAqL1xuXHRbU3ltYm9sLmZvcignbm9kZWpzLnV0aWwuaW5zcGVjdC5jdXN0b20nKV0oKSB7XG5cdFx0cmV0dXJuIFsuLi50aGlzLmtleXMoKV0ucmVkdWNlKChyZXN1bHQsIGtleSkgPT4ge1xuXHRcdFx0Y29uc3QgdmFsdWVzID0gdGhpcy5nZXRBbGwoa2V5KTtcblx0XHRcdC8vIEh0dHAucmVxdWVzdCgpIG9ubHkgc3VwcG9ydHMgc3RyaW5nIGFzIEhvc3QgaGVhZGVyLlxuXHRcdFx0Ly8gVGhpcyBoYWNrIG1ha2VzIHNwZWNpZnlpbmcgY3VzdG9tIEhvc3QgaGVhZGVyIHBvc3NpYmxlLlxuXHRcdFx0aWYgKGtleSA9PT0gJ2hvc3QnKSB7XG5cdFx0XHRcdHJlc3VsdFtrZXldID0gdmFsdWVzWzBdO1xuXHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0cmVzdWx0W2tleV0gPSB2YWx1ZXMubGVuZ3RoID4gMSA/IHZhbHVlcyA6IHZhbHVlc1swXTtcblx0XHRcdH1cblxuXHRcdFx0cmV0dXJuIHJlc3VsdDtcblx0XHR9LCB7fSk7XG5cdH1cbn1cblxuLyoqXG4gKiBSZS1zaGFwaW5nIG9iamVjdCBmb3IgV2ViIElETCB0ZXN0c1xuICogT25seSBuZWVkIHRvIGRvIGl0IGZvciBvdmVycmlkZGVuIG1ldGhvZHNcbiAqL1xuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoXG5cdEhlYWRlcnMucHJvdG90eXBlLFxuXHRbJ2dldCcsICdlbnRyaWVzJywgJ2ZvckVhY2gnLCAndmFsdWVzJ10ucmVkdWNlKChyZXN1bHQsIHByb3BlcnR5KSA9PiB7XG5cdFx0cmVzdWx0W3Byb3BlcnR5XSA9IHtlbnVtZXJhYmxlOiB0cnVlfTtcblx0XHRyZXR1cm4gcmVzdWx0O1xuXHR9LCB7fSlcbik7XG5cbi8qKlxuICogQ3JlYXRlIGEgSGVhZGVycyBvYmplY3QgZnJvbSBhbiBodHRwLkluY29taW5nTWVzc2FnZS5yYXdIZWFkZXJzLCBpZ25vcmluZyB0aG9zZSB0aGF0IGRvXG4gKiBub3QgY29uZm9ybSB0byBIVFRQIGdyYW1tYXIgcHJvZHVjdGlvbnMuXG4gKiBAcGFyYW0ge2ltcG9ydCgnaHR0cCcpLkluY29taW5nTWVzc2FnZVsncmF3SGVhZGVycyddfSBoZWFkZXJzXG4gKi9cbmZ1bmN0aW9uIGZyb21SYXdIZWFkZXJzKGhlYWRlcnMgPSBbXSkge1xuXHRyZXR1cm4gbmV3IEhlYWRlcnMoXG5cdFx0aGVhZGVyc1xuXHRcdFx0Ly8gU3BsaXQgaW50byBwYWlyc1xuXHRcdFx0LnJlZHVjZSgocmVzdWx0LCB2YWx1ZSwgaW5kZXgsIGFycmF5KSA9PiB7XG5cdFx0XHRcdGlmIChpbmRleCAlIDIgPT09IDApIHtcblx0XHRcdFx0XHRyZXN1bHQucHVzaChhcnJheS5zbGljZShpbmRleCwgaW5kZXggKyAyKSk7XG5cdFx0XHRcdH1cblxuXHRcdFx0XHRyZXR1cm4gcmVzdWx0O1xuXHRcdFx0fSwgW10pXG5cdFx0XHQuZmlsdGVyKChbbmFtZSwgdmFsdWVdKSA9PiB7XG5cdFx0XHRcdHRyeSB7XG5cdFx0XHRcdFx0dmFsaWRhdGVIZWFkZXJOYW1lKG5hbWUpO1xuXHRcdFx0XHRcdHZhbGlkYXRlSGVhZGVyVmFsdWUobmFtZSwgU3RyaW5nKHZhbHVlKSk7XG5cdFx0XHRcdFx0cmV0dXJuIHRydWU7XG5cdFx0XHRcdH0gY2F0Y2gge1xuXHRcdFx0XHRcdHJldHVybiBmYWxzZTtcblx0XHRcdFx0fVxuXHRcdFx0fSlcblxuXHQpO1xufVxuXG5jb25zdCByZWRpcmVjdFN0YXR1cyA9IG5ldyBTZXQoWzMwMSwgMzAyLCAzMDMsIDMwNywgMzA4XSk7XG5cbi8qKlxuICogUmVkaXJlY3QgY29kZSBtYXRjaGluZ1xuICpcbiAqIEBwYXJhbSB7bnVtYmVyfSBjb2RlIC0gU3RhdHVzIGNvZGVcbiAqIEByZXR1cm4ge2Jvb2xlYW59XG4gKi9cbmNvbnN0IGlzUmVkaXJlY3QgPSBjb2RlID0+IHtcblx0cmV0dXJuIHJlZGlyZWN0U3RhdHVzLmhhcyhjb2RlKTtcbn07XG5cbi8qKlxuICogUmVzcG9uc2UuanNcbiAqXG4gKiBSZXNwb25zZSBjbGFzcyBwcm92aWRlcyBjb250ZW50IGRlY29kaW5nXG4gKi9cblxuY29uc3QgSU5URVJOQUxTJDEgPSBTeW1ib2woJ1Jlc3BvbnNlIGludGVybmFscycpO1xuXG4vKipcbiAqIFJlc3BvbnNlIGNsYXNzXG4gKlxuICogQHBhcmFtICAgU3RyZWFtICBib2R5ICBSZWFkYWJsZSBzdHJlYW1cbiAqIEBwYXJhbSAgIE9iamVjdCAgb3B0cyAgUmVzcG9uc2Ugb3B0aW9uc1xuICogQHJldHVybiAgVm9pZFxuICovXG5jbGFzcyBSZXNwb25zZSBleHRlbmRzIEJvZHkge1xuXHRjb25zdHJ1Y3Rvcihib2R5ID0gbnVsbCwgb3B0aW9ucyA9IHt9KSB7XG5cdFx0c3VwZXIoYm9keSwgb3B0aW9ucyk7XG5cblx0XHRjb25zdCBzdGF0dXMgPSBvcHRpb25zLnN0YXR1cyB8fCAyMDA7XG5cdFx0Y29uc3QgaGVhZGVycyA9IG5ldyBIZWFkZXJzKG9wdGlvbnMuaGVhZGVycyk7XG5cblx0XHRpZiAoYm9keSAhPT0gbnVsbCAmJiAhaGVhZGVycy5oYXMoJ0NvbnRlbnQtVHlwZScpKSB7XG5cdFx0XHRjb25zdCBjb250ZW50VHlwZSA9IGV4dHJhY3RDb250ZW50VHlwZShib2R5KTtcblx0XHRcdGlmIChjb250ZW50VHlwZSkge1xuXHRcdFx0XHRoZWFkZXJzLmFwcGVuZCgnQ29udGVudC1UeXBlJywgY29udGVudFR5cGUpO1xuXHRcdFx0fVxuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTJDFdID0ge1xuXHRcdFx0dXJsOiBvcHRpb25zLnVybCxcblx0XHRcdHN0YXR1cyxcblx0XHRcdHN0YXR1c1RleHQ6IG9wdGlvbnMuc3RhdHVzVGV4dCB8fCAnJyxcblx0XHRcdGhlYWRlcnMsXG5cdFx0XHRjb3VudGVyOiBvcHRpb25zLmNvdW50ZXIsXG5cdFx0XHRoaWdoV2F0ZXJNYXJrOiBvcHRpb25zLmhpZ2hXYXRlck1hcmtcblx0XHR9O1xuXHR9XG5cblx0Z2V0IHVybCgpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0udXJsIHx8ICcnO1xuXHR9XG5cblx0Z2V0IHN0YXR1cygpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0uc3RhdHVzO1xuXHR9XG5cblx0LyoqXG5cdCAqIENvbnZlbmllbmNlIHByb3BlcnR5IHJlcHJlc2VudGluZyBpZiB0aGUgcmVxdWVzdCBlbmRlZCBub3JtYWxseVxuXHQgKi9cblx0Z2V0IG9rKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5zdGF0dXMgPj0gMjAwICYmIHRoaXNbSU5URVJOQUxTJDFdLnN0YXR1cyA8IDMwMDtcblx0fVxuXG5cdGdldCByZWRpcmVjdGVkKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5jb3VudGVyID4gMDtcblx0fVxuXG5cdGdldCBzdGF0dXNUZXh0KCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQxXS5zdGF0dXNUZXh0O1xuXHR9XG5cblx0Z2V0IGhlYWRlcnMoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDFdLmhlYWRlcnM7XG5cdH1cblxuXHRnZXQgaGlnaFdhdGVyTWFyaygpIHtcblx0XHRyZXR1cm4gdGhpc1tJTlRFUk5BTFMkMV0uaGlnaFdhdGVyTWFyaztcblx0fVxuXG5cdC8qKlxuXHQgKiBDbG9uZSB0aGlzIHJlc3BvbnNlXG5cdCAqXG5cdCAqIEByZXR1cm4gIFJlc3BvbnNlXG5cdCAqL1xuXHRjbG9uZSgpIHtcblx0XHRyZXR1cm4gbmV3IFJlc3BvbnNlKGNsb25lKHRoaXMsIHRoaXMuaGlnaFdhdGVyTWFyayksIHtcblx0XHRcdHVybDogdGhpcy51cmwsXG5cdFx0XHRzdGF0dXM6IHRoaXMuc3RhdHVzLFxuXHRcdFx0c3RhdHVzVGV4dDogdGhpcy5zdGF0dXNUZXh0LFxuXHRcdFx0aGVhZGVyczogdGhpcy5oZWFkZXJzLFxuXHRcdFx0b2s6IHRoaXMub2ssXG5cdFx0XHRyZWRpcmVjdGVkOiB0aGlzLnJlZGlyZWN0ZWQsXG5cdFx0XHRzaXplOiB0aGlzLnNpemVcblx0XHR9KTtcblx0fVxuXG5cdC8qKlxuXHQgKiBAcGFyYW0ge3N0cmluZ30gdXJsICAgIFRoZSBVUkwgdGhhdCB0aGUgbmV3IHJlc3BvbnNlIGlzIHRvIG9yaWdpbmF0ZSBmcm9tLlxuXHQgKiBAcGFyYW0ge251bWJlcn0gc3RhdHVzIEFuIG9wdGlvbmFsIHN0YXR1cyBjb2RlIGZvciB0aGUgcmVzcG9uc2UgKGUuZy4sIDMwMi4pXG5cdCAqIEByZXR1cm5zIHtSZXNwb25zZX0gICAgQSBSZXNwb25zZSBvYmplY3QuXG5cdCAqL1xuXHRzdGF0aWMgcmVkaXJlY3QodXJsLCBzdGF0dXMgPSAzMDIpIHtcblx0XHRpZiAoIWlzUmVkaXJlY3Qoc3RhdHVzKSkge1xuXHRcdFx0dGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ZhaWxlZCB0byBleGVjdXRlIFwicmVkaXJlY3RcIiBvbiBcInJlc3BvbnNlXCI6IEludmFsaWQgc3RhdHVzIGNvZGUnKTtcblx0XHR9XG5cblx0XHRyZXR1cm4gbmV3IFJlc3BvbnNlKG51bGwsIHtcblx0XHRcdGhlYWRlcnM6IHtcblx0XHRcdFx0bG9jYXRpb246IG5ldyBVUkwodXJsKS50b1N0cmluZygpXG5cdFx0XHR9LFxuXHRcdFx0c3RhdHVzXG5cdFx0fSk7XG5cdH1cblxuXHRnZXQgW1N5bWJvbC50b1N0cmluZ1RhZ10oKSB7XG5cdFx0cmV0dXJuICdSZXNwb25zZSc7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVzcG9uc2UucHJvdG90eXBlLCB7XG5cdHVybDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRzdGF0dXM6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0b2s6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0cmVkaXJlY3RlZDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRzdGF0dXNUZXh0OiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGhlYWRlcnM6IHtlbnVtZXJhYmxlOiB0cnVlfSxcblx0Y2xvbmU6IHtlbnVtZXJhYmxlOiB0cnVlfVxufSk7XG5cbmNvbnN0IGdldFNlYXJjaCA9IHBhcnNlZFVSTCA9PiB7XG5cdGlmIChwYXJzZWRVUkwuc2VhcmNoKSB7XG5cdFx0cmV0dXJuIHBhcnNlZFVSTC5zZWFyY2g7XG5cdH1cblxuXHRjb25zdCBsYXN0T2Zmc2V0ID0gcGFyc2VkVVJMLmhyZWYubGVuZ3RoIC0gMTtcblx0Y29uc3QgaGFzaCA9IHBhcnNlZFVSTC5oYXNoIHx8IChwYXJzZWRVUkwuaHJlZltsYXN0T2Zmc2V0XSA9PT0gJyMnID8gJyMnIDogJycpO1xuXHRyZXR1cm4gcGFyc2VkVVJMLmhyZWZbbGFzdE9mZnNldCAtIGhhc2gubGVuZ3RoXSA9PT0gJz8nID8gJz8nIDogJyc7XG59O1xuXG5jb25zdCBJTlRFUk5BTFMkMiA9IFN5bWJvbCgnUmVxdWVzdCBpbnRlcm5hbHMnKTtcblxuLyoqXG4gKiBDaGVjayBpZiBgb2JqYCBpcyBhbiBpbnN0YW5jZSBvZiBSZXF1ZXN0LlxuICpcbiAqIEBwYXJhbSAgeyp9IG9ialxuICogQHJldHVybiB7Ym9vbGVhbn1cbiAqL1xuY29uc3QgaXNSZXF1ZXN0ID0gb2JqZWN0ID0+IHtcblx0cmV0dXJuIChcblx0XHR0eXBlb2Ygb2JqZWN0ID09PSAnb2JqZWN0JyAmJlxuXHRcdHR5cGVvZiBvYmplY3RbSU5URVJOQUxTJDJdID09PSAnb2JqZWN0J1xuXHQpO1xufTtcblxuLyoqXG4gKiBSZXF1ZXN0IGNsYXNzXG4gKlxuICogQHBhcmFtICAgTWl4ZWQgICBpbnB1dCAgVXJsIG9yIFJlcXVlc3QgaW5zdGFuY2VcbiAqIEBwYXJhbSAgIE9iamVjdCAgaW5pdCAgIEN1c3RvbSBvcHRpb25zXG4gKiBAcmV0dXJuICBWb2lkXG4gKi9cbmNsYXNzIFJlcXVlc3QgZXh0ZW5kcyBCb2R5IHtcblx0Y29uc3RydWN0b3IoaW5wdXQsIGluaXQgPSB7fSkge1xuXHRcdGxldCBwYXJzZWRVUkw7XG5cblx0XHQvLyBOb3JtYWxpemUgaW5wdXQgYW5kIGZvcmNlIFVSTCB0byBiZSBlbmNvZGVkIGFzIFVURi04IChodHRwczovL2dpdGh1Yi5jb20vbm9kZS1mZXRjaC9ub2RlLWZldGNoL2lzc3Vlcy8yNDUpXG5cdFx0aWYgKGlzUmVxdWVzdChpbnB1dCkpIHtcblx0XHRcdHBhcnNlZFVSTCA9IG5ldyBVUkwoaW5wdXQudXJsKTtcblx0XHR9IGVsc2Uge1xuXHRcdFx0cGFyc2VkVVJMID0gbmV3IFVSTChpbnB1dCk7XG5cdFx0XHRpbnB1dCA9IHt9O1xuXHRcdH1cblxuXHRcdGxldCBtZXRob2QgPSBpbml0Lm1ldGhvZCB8fCBpbnB1dC5tZXRob2QgfHwgJ0dFVCc7XG5cdFx0bWV0aG9kID0gbWV0aG9kLnRvVXBwZXJDYXNlKCk7XG5cblx0XHQvLyBlc2xpbnQtZGlzYWJsZS1uZXh0LWxpbmUgbm8tZXEtbnVsbCwgZXFlcWVxXG5cdFx0aWYgKCgoaW5pdC5ib2R5ICE9IG51bGwgfHwgaXNSZXF1ZXN0KGlucHV0KSkgJiYgaW5wdXQuYm9keSAhPT0gbnVsbCkgJiZcblx0XHRcdChtZXRob2QgPT09ICdHRVQnIHx8IG1ldGhvZCA9PT0gJ0hFQUQnKSkge1xuXHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcignUmVxdWVzdCB3aXRoIEdFVC9IRUFEIG1ldGhvZCBjYW5ub3QgaGF2ZSBib2R5Jyk7XG5cdFx0fVxuXG5cdFx0Y29uc3QgaW5wdXRCb2R5ID0gaW5pdC5ib2R5ID9cblx0XHRcdGluaXQuYm9keSA6XG5cdFx0XHQoaXNSZXF1ZXN0KGlucHV0KSAmJiBpbnB1dC5ib2R5ICE9PSBudWxsID9cblx0XHRcdFx0Y2xvbmUoaW5wdXQpIDpcblx0XHRcdFx0bnVsbCk7XG5cblx0XHRzdXBlcihpbnB1dEJvZHksIHtcblx0XHRcdHNpemU6IGluaXQuc2l6ZSB8fCBpbnB1dC5zaXplIHx8IDBcblx0XHR9KTtcblxuXHRcdGNvbnN0IGhlYWRlcnMgPSBuZXcgSGVhZGVycyhpbml0LmhlYWRlcnMgfHwgaW5wdXQuaGVhZGVycyB8fCB7fSk7XG5cblx0XHRpZiAoaW5wdXRCb2R5ICE9PSBudWxsICYmICFoZWFkZXJzLmhhcygnQ29udGVudC1UeXBlJykpIHtcblx0XHRcdGNvbnN0IGNvbnRlbnRUeXBlID0gZXh0cmFjdENvbnRlbnRUeXBlKGlucHV0Qm9keSwgdGhpcyk7XG5cdFx0XHRpZiAoY29udGVudFR5cGUpIHtcblx0XHRcdFx0aGVhZGVycy5hcHBlbmQoJ0NvbnRlbnQtVHlwZScsIGNvbnRlbnRUeXBlKTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHRsZXQgc2lnbmFsID0gaXNSZXF1ZXN0KGlucHV0KSA/XG5cdFx0XHRpbnB1dC5zaWduYWwgOlxuXHRcdFx0bnVsbDtcblx0XHRpZiAoJ3NpZ25hbCcgaW4gaW5pdCkge1xuXHRcdFx0c2lnbmFsID0gaW5pdC5zaWduYWw7XG5cdFx0fVxuXG5cdFx0aWYgKHNpZ25hbCAhPT0gbnVsbCAmJiAhaXNBYm9ydFNpZ25hbChzaWduYWwpKSB7XG5cdFx0XHR0aHJvdyBuZXcgVHlwZUVycm9yKCdFeHBlY3RlZCBzaWduYWwgdG8gYmUgYW4gaW5zdGFuY2VvZiBBYm9ydFNpZ25hbCcpO1xuXHRcdH1cblxuXHRcdHRoaXNbSU5URVJOQUxTJDJdID0ge1xuXHRcdFx0bWV0aG9kLFxuXHRcdFx0cmVkaXJlY3Q6IGluaXQucmVkaXJlY3QgfHwgaW5wdXQucmVkaXJlY3QgfHwgJ2ZvbGxvdycsXG5cdFx0XHRoZWFkZXJzLFxuXHRcdFx0cGFyc2VkVVJMLFxuXHRcdFx0c2lnbmFsXG5cdFx0fTtcblxuXHRcdC8vIE5vZGUtZmV0Y2gtb25seSBvcHRpb25zXG5cdFx0dGhpcy5mb2xsb3cgPSBpbml0LmZvbGxvdyA9PT0gdW5kZWZpbmVkID8gKGlucHV0LmZvbGxvdyA9PT0gdW5kZWZpbmVkID8gMjAgOiBpbnB1dC5mb2xsb3cpIDogaW5pdC5mb2xsb3c7XG5cdFx0dGhpcy5jb21wcmVzcyA9IGluaXQuY29tcHJlc3MgPT09IHVuZGVmaW5lZCA/IChpbnB1dC5jb21wcmVzcyA9PT0gdW5kZWZpbmVkID8gdHJ1ZSA6IGlucHV0LmNvbXByZXNzKSA6IGluaXQuY29tcHJlc3M7XG5cdFx0dGhpcy5jb3VudGVyID0gaW5pdC5jb3VudGVyIHx8IGlucHV0LmNvdW50ZXIgfHwgMDtcblx0XHR0aGlzLmFnZW50ID0gaW5pdC5hZ2VudCB8fCBpbnB1dC5hZ2VudDtcblx0XHR0aGlzLmhpZ2hXYXRlck1hcmsgPSBpbml0LmhpZ2hXYXRlck1hcmsgfHwgaW5wdXQuaGlnaFdhdGVyTWFyayB8fCAxNjM4NDtcblx0XHR0aGlzLmluc2VjdXJlSFRUUFBhcnNlciA9IGluaXQuaW5zZWN1cmVIVFRQUGFyc2VyIHx8IGlucHV0Lmluc2VjdXJlSFRUUFBhcnNlciB8fCBmYWxzZTtcblx0fVxuXG5cdGdldCBtZXRob2QoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDJdLm1ldGhvZDtcblx0fVxuXG5cdGdldCB1cmwoKSB7XG5cdFx0cmV0dXJuIHVybC5mb3JtYXQodGhpc1tJTlRFUk5BTFMkMl0ucGFyc2VkVVJMKTtcblx0fVxuXG5cdGdldCBoZWFkZXJzKCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQyXS5oZWFkZXJzO1xuXHR9XG5cblx0Z2V0IHJlZGlyZWN0KCkge1xuXHRcdHJldHVybiB0aGlzW0lOVEVSTkFMUyQyXS5yZWRpcmVjdDtcblx0fVxuXG5cdGdldCBzaWduYWwoKSB7XG5cdFx0cmV0dXJuIHRoaXNbSU5URVJOQUxTJDJdLnNpZ25hbDtcblx0fVxuXG5cdC8qKlxuXHQgKiBDbG9uZSB0aGlzIHJlcXVlc3Rcblx0ICpcblx0ICogQHJldHVybiAgUmVxdWVzdFxuXHQgKi9cblx0Y2xvbmUoKSB7XG5cdFx0cmV0dXJuIG5ldyBSZXF1ZXN0KHRoaXMpO1xuXHR9XG5cblx0Z2V0IFtTeW1ib2wudG9TdHJpbmdUYWddKCkge1xuXHRcdHJldHVybiAnUmVxdWVzdCc7XG5cdH1cbn1cblxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVxdWVzdC5wcm90b3R5cGUsIHtcblx0bWV0aG9kOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHVybDoge2VudW1lcmFibGU6IHRydWV9LFxuXHRoZWFkZXJzOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHJlZGlyZWN0OiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdGNsb25lOiB7ZW51bWVyYWJsZTogdHJ1ZX0sXG5cdHNpZ25hbDoge2VudW1lcmFibGU6IHRydWV9XG59KTtcblxuLyoqXG4gKiBDb252ZXJ0IGEgUmVxdWVzdCB0byBOb2RlLmpzIGh0dHAgcmVxdWVzdCBvcHRpb25zLlxuICpcbiAqIEBwYXJhbSAgIFJlcXVlc3QgIEEgUmVxdWVzdCBpbnN0YW5jZVxuICogQHJldHVybiAgT2JqZWN0ICAgVGhlIG9wdGlvbnMgb2JqZWN0IHRvIGJlIHBhc3NlZCB0byBodHRwLnJlcXVlc3RcbiAqL1xuY29uc3QgZ2V0Tm9kZVJlcXVlc3RPcHRpb25zID0gcmVxdWVzdCA9PiB7XG5cdGNvbnN0IHtwYXJzZWRVUkx9ID0gcmVxdWVzdFtJTlRFUk5BTFMkMl07XG5cdGNvbnN0IGhlYWRlcnMgPSBuZXcgSGVhZGVycyhyZXF1ZXN0W0lOVEVSTkFMUyQyXS5oZWFkZXJzKTtcblxuXHQvLyBGZXRjaCBzdGVwIDEuM1xuXHRpZiAoIWhlYWRlcnMuaGFzKCdBY2NlcHQnKSkge1xuXHRcdGhlYWRlcnMuc2V0KCdBY2NlcHQnLCAnKi8qJyk7XG5cdH1cblxuXHQvLyBIVFRQLW5ldHdvcmstb3ItY2FjaGUgZmV0Y2ggc3RlcHMgMi40LTIuN1xuXHRsZXQgY29udGVudExlbmd0aFZhbHVlID0gbnVsbDtcblx0aWYgKHJlcXVlc3QuYm9keSA9PT0gbnVsbCAmJiAvXihwb3N0fHB1dCkkL2kudGVzdChyZXF1ZXN0Lm1ldGhvZCkpIHtcblx0XHRjb250ZW50TGVuZ3RoVmFsdWUgPSAnMCc7XG5cdH1cblxuXHRpZiAocmVxdWVzdC5ib2R5ICE9PSBudWxsKSB7XG5cdFx0Y29uc3QgdG90YWxCeXRlcyA9IGdldFRvdGFsQnl0ZXMocmVxdWVzdCk7XG5cdFx0Ly8gU2V0IENvbnRlbnQtTGVuZ3RoIGlmIHRvdGFsQnl0ZXMgaXMgYSBudW1iZXIgKHRoYXQgaXMgbm90IE5hTilcblx0XHRpZiAodHlwZW9mIHRvdGFsQnl0ZXMgPT09ICdudW1iZXInICYmICFOdW1iZXIuaXNOYU4odG90YWxCeXRlcykpIHtcblx0XHRcdGNvbnRlbnRMZW5ndGhWYWx1ZSA9IFN0cmluZyh0b3RhbEJ5dGVzKTtcblx0XHR9XG5cdH1cblxuXHRpZiAoY29udGVudExlbmd0aFZhbHVlKSB7XG5cdFx0aGVhZGVycy5zZXQoJ0NvbnRlbnQtTGVuZ3RoJywgY29udGVudExlbmd0aFZhbHVlKTtcblx0fVxuXG5cdC8vIEhUVFAtbmV0d29yay1vci1jYWNoZSBmZXRjaCBzdGVwIDIuMTFcblx0aWYgKCFoZWFkZXJzLmhhcygnVXNlci1BZ2VudCcpKSB7XG5cdFx0aGVhZGVycy5zZXQoJ1VzZXItQWdlbnQnLCAnbm9kZS1mZXRjaCcpO1xuXHR9XG5cblx0Ly8gSFRUUC1uZXR3b3JrLW9yLWNhY2hlIGZldGNoIHN0ZXAgMi4xNVxuXHRpZiAocmVxdWVzdC5jb21wcmVzcyAmJiAhaGVhZGVycy5oYXMoJ0FjY2VwdC1FbmNvZGluZycpKSB7XG5cdFx0aGVhZGVycy5zZXQoJ0FjY2VwdC1FbmNvZGluZycsICdnemlwLGRlZmxhdGUsYnInKTtcblx0fVxuXG5cdGxldCB7YWdlbnR9ID0gcmVxdWVzdDtcblx0aWYgKHR5cGVvZiBhZ2VudCA9PT0gJ2Z1bmN0aW9uJykge1xuXHRcdGFnZW50ID0gYWdlbnQocGFyc2VkVVJMKTtcblx0fVxuXG5cdGlmICghaGVhZGVycy5oYXMoJ0Nvbm5lY3Rpb24nKSAmJiAhYWdlbnQpIHtcblx0XHRoZWFkZXJzLnNldCgnQ29ubmVjdGlvbicsICdjbG9zZScpO1xuXHR9XG5cblx0Ly8gSFRUUC1uZXR3b3JrIGZldGNoIHN0ZXAgNC4yXG5cdC8vIGNodW5rZWQgZW5jb2RpbmcgaXMgaGFuZGxlZCBieSBOb2RlLmpzXG5cblx0Y29uc3Qgc2VhcmNoID0gZ2V0U2VhcmNoKHBhcnNlZFVSTCk7XG5cblx0Ly8gTWFudWFsbHkgc3ByZWFkIHRoZSBVUkwgb2JqZWN0IGluc3RlYWQgb2Ygc3ByZWFkIHN5bnRheFxuXHRjb25zdCByZXF1ZXN0T3B0aW9ucyA9IHtcblx0XHRwYXRoOiBwYXJzZWRVUkwucGF0aG5hbWUgKyBzZWFyY2gsXG5cdFx0cGF0aG5hbWU6IHBhcnNlZFVSTC5wYXRobmFtZSxcblx0XHRob3N0bmFtZTogcGFyc2VkVVJMLmhvc3RuYW1lLFxuXHRcdHByb3RvY29sOiBwYXJzZWRVUkwucHJvdG9jb2wsXG5cdFx0cG9ydDogcGFyc2VkVVJMLnBvcnQsXG5cdFx0aGFzaDogcGFyc2VkVVJMLmhhc2gsXG5cdFx0c2VhcmNoOiBwYXJzZWRVUkwuc2VhcmNoLFxuXHRcdHF1ZXJ5OiBwYXJzZWRVUkwucXVlcnksXG5cdFx0aHJlZjogcGFyc2VkVVJMLmhyZWYsXG5cdFx0bWV0aG9kOiByZXF1ZXN0Lm1ldGhvZCxcblx0XHRoZWFkZXJzOiBoZWFkZXJzW1N5bWJvbC5mb3IoJ25vZGVqcy51dGlsLmluc3BlY3QuY3VzdG9tJyldKCksXG5cdFx0aW5zZWN1cmVIVFRQUGFyc2VyOiByZXF1ZXN0Lmluc2VjdXJlSFRUUFBhcnNlcixcblx0XHRhZ2VudFxuXHR9O1xuXG5cdHJldHVybiByZXF1ZXN0T3B0aW9ucztcbn07XG5cbi8qKlxuICogQWJvcnRFcnJvciBpbnRlcmZhY2UgZm9yIGNhbmNlbGxlZCByZXF1ZXN0c1xuICovXG5jbGFzcyBBYm9ydEVycm9yIGV4dGVuZHMgRmV0Y2hCYXNlRXJyb3Ige1xuXHRjb25zdHJ1Y3RvcihtZXNzYWdlLCB0eXBlID0gJ2Fib3J0ZWQnKSB7XG5cdFx0c3VwZXIobWVzc2FnZSwgdHlwZSk7XG5cdH1cbn1cblxuLyoqXG4gKiBJbmRleC5qc1xuICpcbiAqIGEgcmVxdWVzdCBBUEkgY29tcGF0aWJsZSB3aXRoIHdpbmRvdy5mZXRjaFxuICpcbiAqIEFsbCBzcGVjIGFsZ29yaXRobSBzdGVwIG51bWJlcnMgYXJlIGJhc2VkIG9uIGh0dHBzOi8vZmV0Y2guc3BlYy53aGF0d2cub3JnL2NvbW1pdC1zbmFwc2hvdHMvYWU3MTY4MjJjYjNhNjE4NDMyMjZjZDA5MGVlZmM2NTg5NDQ2YzFkMi8uXG4gKi9cblxuY29uc3Qgc3VwcG9ydGVkU2NoZW1hcyA9IG5ldyBTZXQoWydkYXRhOicsICdodHRwOicsICdodHRwczonXSk7XG5cbi8qKlxuICogRmV0Y2ggZnVuY3Rpb25cbiAqXG4gKiBAcGFyYW0gICB7c3RyaW5nIHwgVVJMIHwgaW1wb3J0KCcuL3JlcXVlc3QnKS5kZWZhdWx0fSB1cmwgLSBBYnNvbHV0ZSB1cmwgb3IgUmVxdWVzdCBpbnN0YW5jZVxuICogQHBhcmFtICAgeyp9IFtvcHRpb25zX10gLSBGZXRjaCBvcHRpb25zXG4gKiBAcmV0dXJuICB7UHJvbWlzZTxpbXBvcnQoJy4vcmVzcG9uc2UnKS5kZWZhdWx0Pn1cbiAqL1xuYXN5bmMgZnVuY3Rpb24gZmV0Y2godXJsLCBvcHRpb25zXykge1xuXHRyZXR1cm4gbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuXHRcdC8vIEJ1aWxkIHJlcXVlc3Qgb2JqZWN0XG5cdFx0Y29uc3QgcmVxdWVzdCA9IG5ldyBSZXF1ZXN0KHVybCwgb3B0aW9uc18pO1xuXHRcdGNvbnN0IG9wdGlvbnMgPSBnZXROb2RlUmVxdWVzdE9wdGlvbnMocmVxdWVzdCk7XG5cdFx0aWYgKCFzdXBwb3J0ZWRTY2hlbWFzLmhhcyhvcHRpb25zLnByb3RvY29sKSkge1xuXHRcdFx0dGhyb3cgbmV3IFR5cGVFcnJvcihgbm9kZS1mZXRjaCBjYW5ub3QgbG9hZCAke3VybH0uIFVSTCBzY2hlbWUgXCIke29wdGlvbnMucHJvdG9jb2wucmVwbGFjZSgvOiQvLCAnJyl9XCIgaXMgbm90IHN1cHBvcnRlZC5gKTtcblx0XHR9XG5cblx0XHRpZiAob3B0aW9ucy5wcm90b2NvbCA9PT0gJ2RhdGE6Jykge1xuXHRcdFx0Y29uc3QgZGF0YSA9IGRhdGFVcmlUb0J1ZmZlcihyZXF1ZXN0LnVybCk7XG5cdFx0XHRjb25zdCByZXNwb25zZSA9IG5ldyBSZXNwb25zZShkYXRhLCB7aGVhZGVyczogeydDb250ZW50LVR5cGUnOiBkYXRhLnR5cGVGdWxsfX0pO1xuXHRcdFx0cmVzb2x2ZShyZXNwb25zZSk7XG5cdFx0XHRyZXR1cm47XG5cdFx0fVxuXG5cdFx0Ly8gV3JhcCBodHRwLnJlcXVlc3QgaW50byBmZXRjaFxuXHRcdGNvbnN0IHNlbmQgPSAob3B0aW9ucy5wcm90b2NvbCA9PT0gJ2h0dHBzOicgPyBodHRwcyA6IGh0dHApLnJlcXVlc3Q7XG5cdFx0Y29uc3Qge3NpZ25hbH0gPSByZXF1ZXN0O1xuXHRcdGxldCByZXNwb25zZSA9IG51bGw7XG5cblx0XHRjb25zdCBhYm9ydCA9ICgpID0+IHtcblx0XHRcdGNvbnN0IGVycm9yID0gbmV3IEFib3J0RXJyb3IoJ1RoZSBvcGVyYXRpb24gd2FzIGFib3J0ZWQuJyk7XG5cdFx0XHRyZWplY3QoZXJyb3IpO1xuXHRcdFx0aWYgKHJlcXVlc3QuYm9keSAmJiByZXF1ZXN0LmJvZHkgaW5zdGFuY2VvZiBTdHJlYW0uUmVhZGFibGUpIHtcblx0XHRcdFx0cmVxdWVzdC5ib2R5LmRlc3Ryb3koZXJyb3IpO1xuXHRcdFx0fVxuXG5cdFx0XHRpZiAoIXJlc3BvbnNlIHx8ICFyZXNwb25zZS5ib2R5KSB7XG5cdFx0XHRcdHJldHVybjtcblx0XHRcdH1cblxuXHRcdFx0cmVzcG9uc2UuYm9keS5lbWl0KCdlcnJvcicsIGVycm9yKTtcblx0XHR9O1xuXG5cdFx0aWYgKHNpZ25hbCAmJiBzaWduYWwuYWJvcnRlZCkge1xuXHRcdFx0YWJvcnQoKTtcblx0XHRcdHJldHVybjtcblx0XHR9XG5cblx0XHRjb25zdCBhYm9ydEFuZEZpbmFsaXplID0gKCkgPT4ge1xuXHRcdFx0YWJvcnQoKTtcblx0XHRcdGZpbmFsaXplKCk7XG5cdFx0fTtcblxuXHRcdC8vIFNlbmQgcmVxdWVzdFxuXHRcdGNvbnN0IHJlcXVlc3RfID0gc2VuZChvcHRpb25zKTtcblxuXHRcdGlmIChzaWduYWwpIHtcblx0XHRcdHNpZ25hbC5hZGRFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QW5kRmluYWxpemUpO1xuXHRcdH1cblxuXHRcdGNvbnN0IGZpbmFsaXplID0gKCkgPT4ge1xuXHRcdFx0cmVxdWVzdF8uYWJvcnQoKTtcblx0XHRcdGlmIChzaWduYWwpIHtcblx0XHRcdFx0c2lnbmFsLnJlbW92ZUV2ZW50TGlzdGVuZXIoJ2Fib3J0JywgYWJvcnRBbmRGaW5hbGl6ZSk7XG5cdFx0XHR9XG5cdFx0fTtcblxuXHRcdHJlcXVlc3RfLm9uKCdlcnJvcicsIGVyciA9PiB7XG5cdFx0XHRyZWplY3QobmV3IEZldGNoRXJyb3IoYHJlcXVlc3QgdG8gJHtyZXF1ZXN0LnVybH0gZmFpbGVkLCByZWFzb246ICR7ZXJyLm1lc3NhZ2V9YCwgJ3N5c3RlbScsIGVycikpO1xuXHRcdFx0ZmluYWxpemUoKTtcblx0XHR9KTtcblxuXHRcdHJlcXVlc3RfLm9uKCdyZXNwb25zZScsIHJlc3BvbnNlXyA9PiB7XG5cdFx0XHRyZXF1ZXN0Xy5zZXRUaW1lb3V0KDApO1xuXHRcdFx0Y29uc3QgaGVhZGVycyA9IGZyb21SYXdIZWFkZXJzKHJlc3BvbnNlXy5yYXdIZWFkZXJzKTtcblxuXHRcdFx0Ly8gSFRUUCBmZXRjaCBzdGVwIDVcblx0XHRcdGlmIChpc1JlZGlyZWN0KHJlc3BvbnNlXy5zdGF0dXNDb2RlKSkge1xuXHRcdFx0XHQvLyBIVFRQIGZldGNoIHN0ZXAgNS4yXG5cdFx0XHRcdGNvbnN0IGxvY2F0aW9uID0gaGVhZGVycy5nZXQoJ0xvY2F0aW9uJyk7XG5cblx0XHRcdFx0Ly8gSFRUUCBmZXRjaCBzdGVwIDUuM1xuXHRcdFx0XHRjb25zdCBsb2NhdGlvblVSTCA9IGxvY2F0aW9uID09PSBudWxsID8gbnVsbCA6IG5ldyBVUkwobG9jYXRpb24sIHJlcXVlc3QudXJsKTtcblxuXHRcdFx0XHQvLyBIVFRQIGZldGNoIHN0ZXAgNS41XG5cdFx0XHRcdHN3aXRjaCAocmVxdWVzdC5yZWRpcmVjdCkge1xuXHRcdFx0XHRcdGNhc2UgJ2Vycm9yJzpcblx0XHRcdFx0XHRcdHJlamVjdChuZXcgRmV0Y2hFcnJvcihgdXJpIHJlcXVlc3RlZCByZXNwb25kcyB3aXRoIGEgcmVkaXJlY3QsIHJlZGlyZWN0IG1vZGUgaXMgc2V0IHRvIGVycm9yOiAke3JlcXVlc3QudXJsfWAsICduby1yZWRpcmVjdCcpKTtcblx0XHRcdFx0XHRcdGZpbmFsaXplKCk7XG5cdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0Y2FzZSAnbWFudWFsJzpcblx0XHRcdFx0XHRcdC8vIE5vZGUtZmV0Y2gtc3BlY2lmaWMgc3RlcDogbWFrZSBtYW51YWwgcmVkaXJlY3QgYSBiaXQgZWFzaWVyIHRvIHVzZSBieSBzZXR0aW5nIHRoZSBMb2NhdGlvbiBoZWFkZXIgdmFsdWUgdG8gdGhlIHJlc29sdmVkIFVSTC5cblx0XHRcdFx0XHRcdGlmIChsb2NhdGlvblVSTCAhPT0gbnVsbCkge1xuXHRcdFx0XHRcdFx0XHQvLyBIYW5kbGUgY29ycnVwdGVkIGhlYWRlclxuXHRcdFx0XHRcdFx0XHR0cnkge1xuXHRcdFx0XHRcdFx0XHRcdGhlYWRlcnMuc2V0KCdMb2NhdGlvbicsIGxvY2F0aW9uVVJMKTtcblx0XHRcdFx0XHRcdFx0XHQvKiBjOCBpZ25vcmUgbmV4dCAzICovXG5cdFx0XHRcdFx0XHRcdH0gY2F0Y2ggKGVycm9yKSB7XG5cdFx0XHRcdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0XHRcdFx0fVxuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHRicmVhaztcblx0XHRcdFx0XHRjYXNlICdmb2xsb3cnOiB7XG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgMlxuXHRcdFx0XHRcdFx0aWYgKGxvY2F0aW9uVVJMID09PSBudWxsKSB7XG5cdFx0XHRcdFx0XHRcdGJyZWFrO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgNVxuXHRcdFx0XHRcdFx0aWYgKHJlcXVlc3QuY291bnRlciA+PSByZXF1ZXN0LmZvbGxvdykge1xuXHRcdFx0XHRcdFx0XHRyZWplY3QobmV3IEZldGNoRXJyb3IoYG1heGltdW0gcmVkaXJlY3QgcmVhY2hlZCBhdDogJHtyZXF1ZXN0LnVybH1gLCAnbWF4LXJlZGlyZWN0JykpO1xuXHRcdFx0XHRcdFx0XHRmaW5hbGl6ZSgpO1xuXHRcdFx0XHRcdFx0XHRyZXR1cm47XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdC8vIEhUVFAtcmVkaXJlY3QgZmV0Y2ggc3RlcCA2IChjb3VudGVyIGluY3JlbWVudClcblx0XHRcdFx0XHRcdC8vIENyZWF0ZSBhIG5ldyBSZXF1ZXN0IG9iamVjdC5cblx0XHRcdFx0XHRcdGNvbnN0IHJlcXVlc3RPcHRpb25zID0ge1xuXHRcdFx0XHRcdFx0XHRoZWFkZXJzOiBuZXcgSGVhZGVycyhyZXF1ZXN0LmhlYWRlcnMpLFxuXHRcdFx0XHRcdFx0XHRmb2xsb3c6IHJlcXVlc3QuZm9sbG93LFxuXHRcdFx0XHRcdFx0XHRjb3VudGVyOiByZXF1ZXN0LmNvdW50ZXIgKyAxLFxuXHRcdFx0XHRcdFx0XHRhZ2VudDogcmVxdWVzdC5hZ2VudCxcblx0XHRcdFx0XHRcdFx0Y29tcHJlc3M6IHJlcXVlc3QuY29tcHJlc3MsXG5cdFx0XHRcdFx0XHRcdG1ldGhvZDogcmVxdWVzdC5tZXRob2QsXG5cdFx0XHRcdFx0XHRcdGJvZHk6IHJlcXVlc3QuYm9keSxcblx0XHRcdFx0XHRcdFx0c2lnbmFsOiByZXF1ZXN0LnNpZ25hbCxcblx0XHRcdFx0XHRcdFx0c2l6ZTogcmVxdWVzdC5zaXplXG5cdFx0XHRcdFx0XHR9O1xuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgOVxuXHRcdFx0XHRcdFx0aWYgKHJlc3BvbnNlXy5zdGF0dXNDb2RlICE9PSAzMDMgJiYgcmVxdWVzdC5ib2R5ICYmIG9wdGlvbnNfLmJvZHkgaW5zdGFuY2VvZiBTdHJlYW0uUmVhZGFibGUpIHtcblx0XHRcdFx0XHRcdFx0cmVqZWN0KG5ldyBGZXRjaEVycm9yKCdDYW5ub3QgZm9sbG93IHJlZGlyZWN0IHdpdGggYm9keSBiZWluZyBhIHJlYWRhYmxlIHN0cmVhbScsICd1bnN1cHBvcnRlZC1yZWRpcmVjdCcpKTtcblx0XHRcdFx0XHRcdFx0ZmluYWxpemUoKTtcblx0XHRcdFx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0XHRcdFx0fVxuXG5cdFx0XHRcdFx0XHQvLyBIVFRQLXJlZGlyZWN0IGZldGNoIHN0ZXAgMTFcblx0XHRcdFx0XHRcdGlmIChyZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzAzIHx8ICgocmVzcG9uc2VfLnN0YXR1c0NvZGUgPT09IDMwMSB8fCByZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzAyKSAmJiByZXF1ZXN0Lm1ldGhvZCA9PT0gJ1BPU1QnKSkge1xuXHRcdFx0XHRcdFx0XHRyZXF1ZXN0T3B0aW9ucy5tZXRob2QgPSAnR0VUJztcblx0XHRcdFx0XHRcdFx0cmVxdWVzdE9wdGlvbnMuYm9keSA9IHVuZGVmaW5lZDtcblx0XHRcdFx0XHRcdFx0cmVxdWVzdE9wdGlvbnMuaGVhZGVycy5kZWxldGUoJ2NvbnRlbnQtbGVuZ3RoJyk7XG5cdFx0XHRcdFx0XHR9XG5cblx0XHRcdFx0XHRcdC8vIEhUVFAtcmVkaXJlY3QgZmV0Y2ggc3RlcCAxNVxuXHRcdFx0XHRcdFx0cmVzb2x2ZShmZXRjaChuZXcgUmVxdWVzdChsb2NhdGlvblVSTCwgcmVxdWVzdE9wdGlvbnMpKSk7XG5cdFx0XHRcdFx0XHRmaW5hbGl6ZSgpO1xuXHRcdFx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0XHRcdH1cblx0XHRcdFx0XHQvLyBEbyBub3RoaW5nXG5cdFx0XHRcdH1cblx0XHRcdH1cblxuXHRcdFx0Ly8gUHJlcGFyZSByZXNwb25zZVxuXHRcdFx0cmVzcG9uc2VfLm9uY2UoJ2VuZCcsICgpID0+IHtcblx0XHRcdFx0aWYgKHNpZ25hbCkge1xuXHRcdFx0XHRcdHNpZ25hbC5yZW1vdmVFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QW5kRmluYWxpemUpO1xuXHRcdFx0XHR9XG5cdFx0XHR9KTtcblxuXHRcdFx0bGV0IGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUocmVzcG9uc2VfLCBuZXcgU3RyZWFtLlBhc3NUaHJvdWdoKCksIGVycm9yID0+IHtcblx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdH0pO1xuXHRcdFx0Ly8gc2VlIGh0dHBzOi8vZ2l0aHViLmNvbS9ub2RlanMvbm9kZS9wdWxsLzI5Mzc2XG5cdFx0XHRpZiAocHJvY2Vzcy52ZXJzaW9uIDwgJ3YxMi4xMCcpIHtcblx0XHRcdFx0cmVzcG9uc2VfLm9uKCdhYm9ydGVkJywgYWJvcnRBbmRGaW5hbGl6ZSk7XG5cdFx0XHR9XG5cblx0XHRcdGNvbnN0IHJlc3BvbnNlT3B0aW9ucyA9IHtcblx0XHRcdFx0dXJsOiByZXF1ZXN0LnVybCxcblx0XHRcdFx0c3RhdHVzOiByZXNwb25zZV8uc3RhdHVzQ29kZSxcblx0XHRcdFx0c3RhdHVzVGV4dDogcmVzcG9uc2VfLnN0YXR1c01lc3NhZ2UsXG5cdFx0XHRcdGhlYWRlcnMsXG5cdFx0XHRcdHNpemU6IHJlcXVlc3Quc2l6ZSxcblx0XHRcdFx0Y291bnRlcjogcmVxdWVzdC5jb3VudGVyLFxuXHRcdFx0XHRoaWdoV2F0ZXJNYXJrOiByZXF1ZXN0LmhpZ2hXYXRlck1hcmtcblx0XHRcdH07XG5cblx0XHRcdC8vIEhUVFAtbmV0d29yayBmZXRjaCBzdGVwIDEyLjEuMS4zXG5cdFx0XHRjb25zdCBjb2RpbmdzID0gaGVhZGVycy5nZXQoJ0NvbnRlbnQtRW5jb2RpbmcnKTtcblxuXHRcdFx0Ly8gSFRUUC1uZXR3b3JrIGZldGNoIHN0ZXAgMTIuMS4xLjQ6IGhhbmRsZSBjb250ZW50IGNvZGluZ3NcblxuXHRcdFx0Ly8gaW4gZm9sbG93aW5nIHNjZW5hcmlvcyB3ZSBpZ25vcmUgY29tcHJlc3Npb24gc3VwcG9ydFxuXHRcdFx0Ly8gMS4gY29tcHJlc3Npb24gc3VwcG9ydCBpcyBkaXNhYmxlZFxuXHRcdFx0Ly8gMi4gSEVBRCByZXF1ZXN0XG5cdFx0XHQvLyAzLiBubyBDb250ZW50LUVuY29kaW5nIGhlYWRlclxuXHRcdFx0Ly8gNC4gbm8gY29udGVudCByZXNwb25zZSAoMjA0KVxuXHRcdFx0Ly8gNS4gY29udGVudCBub3QgbW9kaWZpZWQgcmVzcG9uc2UgKDMwNClcblx0XHRcdGlmICghcmVxdWVzdC5jb21wcmVzcyB8fCByZXF1ZXN0Lm1ldGhvZCA9PT0gJ0hFQUQnIHx8IGNvZGluZ3MgPT09IG51bGwgfHwgcmVzcG9uc2VfLnN0YXR1c0NvZGUgPT09IDIwNCB8fCByZXNwb25zZV8uc3RhdHVzQ29kZSA9PT0gMzA0KSB7XG5cdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBOb2RlIHY2K1xuXHRcdFx0Ly8gQmUgbGVzcyBzdHJpY3Qgd2hlbiBkZWNvZGluZyBjb21wcmVzc2VkIHJlc3BvbnNlcywgc2luY2Ugc29tZXRpbWVzXG5cdFx0XHQvLyBzZXJ2ZXJzIHNlbmQgc2xpZ2h0bHkgaW52YWxpZCByZXNwb25zZXMgdGhhdCBhcmUgc3RpbGwgYWNjZXB0ZWRcblx0XHRcdC8vIGJ5IGNvbW1vbiBicm93c2Vycy5cblx0XHRcdC8vIEFsd2F5cyB1c2luZyBaX1NZTkNfRkxVU0ggaXMgd2hhdCBjVVJMIGRvZXMuXG5cdFx0XHRjb25zdCB6bGliT3B0aW9ucyA9IHtcblx0XHRcdFx0Zmx1c2g6IHpsaWIuWl9TWU5DX0ZMVVNILFxuXHRcdFx0XHRmaW5pc2hGbHVzaDogemxpYi5aX1NZTkNfRkxVU0hcblx0XHRcdH07XG5cblx0XHRcdC8vIEZvciBnemlwXG5cdFx0XHRpZiAoY29kaW5ncyA9PT0gJ2d6aXAnIHx8IGNvZGluZ3MgPT09ICd4LWd6aXAnKSB7XG5cdFx0XHRcdGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUoYm9keSwgemxpYi5jcmVhdGVHdW56aXAoemxpYk9wdGlvbnMpLCBlcnJvciA9PiB7XG5cdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0fSk7XG5cdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBkZWZsYXRlXG5cdFx0XHRpZiAoY29kaW5ncyA9PT0gJ2RlZmxhdGUnIHx8IGNvZGluZ3MgPT09ICd4LWRlZmxhdGUnKSB7XG5cdFx0XHRcdC8vIEhhbmRsZSB0aGUgaW5mYW1vdXMgcmF3IGRlZmxhdGUgcmVzcG9uc2UgZnJvbSBvbGQgc2VydmVyc1xuXHRcdFx0XHQvLyBhIGhhY2sgZm9yIG9sZCBJSVMgYW5kIEFwYWNoZSBzZXJ2ZXJzXG5cdFx0XHRcdGNvbnN0IHJhdyA9IFN0cmVhbS5waXBlbGluZShyZXNwb25zZV8sIG5ldyBTdHJlYW0uUGFzc1Rocm91Z2goKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdHJlamVjdChlcnJvcik7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyYXcub25jZSgnZGF0YScsIGNodW5rID0+IHtcblx0XHRcdFx0XHQvLyBTZWUgaHR0cDovL3N0YWNrb3ZlcmZsb3cuY29tL3F1ZXN0aW9ucy8zNzUxOTgyOFxuXHRcdFx0XHRcdGlmICgoY2h1bmtbMF0gJiAweDBGKSA9PT0gMHgwOCkge1xuXHRcdFx0XHRcdFx0Ym9keSA9IFN0cmVhbS5waXBlbGluZShib2R5LCB6bGliLmNyZWF0ZUluZmxhdGUoKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdFx0XHRyZWplY3QoZXJyb3IpO1xuXHRcdFx0XHRcdFx0fSk7XG5cdFx0XHRcdFx0fSBlbHNlIHtcblx0XHRcdFx0XHRcdGJvZHkgPSBTdHJlYW0ucGlwZWxpbmUoYm9keSwgemxpYi5jcmVhdGVJbmZsYXRlUmF3KCksIGVycm9yID0+IHtcblx0XHRcdFx0XHRcdFx0cmVqZWN0KGVycm9yKTtcblx0XHRcdFx0XHRcdH0pO1xuXHRcdFx0XHRcdH1cblxuXHRcdFx0XHRcdHJlc3BvbnNlID0gbmV3IFJlc3BvbnNlKGJvZHksIHJlc3BvbnNlT3B0aW9ucyk7XG5cdFx0XHRcdFx0cmVzb2x2ZShyZXNwb25zZSk7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyZXR1cm47XG5cdFx0XHR9XG5cblx0XHRcdC8vIEZvciBiclxuXHRcdFx0aWYgKGNvZGluZ3MgPT09ICdicicpIHtcblx0XHRcdFx0Ym9keSA9IFN0cmVhbS5waXBlbGluZShib2R5LCB6bGliLmNyZWF0ZUJyb3RsaURlY29tcHJlc3MoKSwgZXJyb3IgPT4ge1xuXHRcdFx0XHRcdHJlamVjdChlcnJvcik7XG5cdFx0XHRcdH0pO1xuXHRcdFx0XHRyZXNwb25zZSA9IG5ldyBSZXNwb25zZShib2R5LCByZXNwb25zZU9wdGlvbnMpO1xuXHRcdFx0XHRyZXNvbHZlKHJlc3BvbnNlKTtcblx0XHRcdFx0cmV0dXJuO1xuXHRcdFx0fVxuXG5cdFx0XHQvLyBPdGhlcndpc2UsIHVzZSByZXNwb25zZSBhcy1pc1xuXHRcdFx0cmVzcG9uc2UgPSBuZXcgUmVzcG9uc2UoYm9keSwgcmVzcG9uc2VPcHRpb25zKTtcblx0XHRcdHJlc29sdmUocmVzcG9uc2UpO1xuXHRcdH0pO1xuXG5cdFx0d3JpdGVUb1N0cmVhbShyZXF1ZXN0XywgcmVxdWVzdCk7XG5cdH0pO1xufVxuXG5leHBvcnRzLkFib3J0RXJyb3IgPSBBYm9ydEVycm9yO1xuZXhwb3J0cy5GZXRjaEVycm9yID0gRmV0Y2hFcnJvcjtcbmV4cG9ydHMuSGVhZGVycyA9IEhlYWRlcnM7XG5leHBvcnRzLlJlcXVlc3QgPSBSZXF1ZXN0O1xuZXhwb3J0cy5SZXNwb25zZSA9IFJlc3BvbnNlO1xuZXhwb3J0cy5kZWZhdWx0ID0gZmV0Y2g7XG5leHBvcnRzLmlzUmVkaXJlY3QgPSBpc1JlZGlyZWN0O1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9aW5kZXguY2pzLm1hcFxuIiwiZnVuY3Rpb24gbm9ybWFsaXplIChzdHIpIHtcbiAgcmV0dXJuIHN0clxuICAgICAgICAgIC5yZXBsYWNlKC9bXFwvXSsvZywgJy8nKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXC9cXD8vZywgJz8nKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXC9cXCMvZywgJyMnKVxuICAgICAgICAgIC5yZXBsYWNlKC9cXDpcXC8vZywgJzovLycpO1xufVxuXG5tb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uICgpIHtcbiAgdmFyIGpvaW5lZCA9IFtdLnNsaWNlLmNhbGwoYXJndW1lbnRzLCAwKS5qb2luKCcvJyk7XG4gIHJldHVybiBub3JtYWxpemUoam9pbmVkKTtcbn07IiwiLyoqXG4gKiB3ZWItc3RyZWFtcy1wb2x5ZmlsbCB2My4wLjFcbiAqL1xuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LnN5bWJvbFwiIC8+XG5jb25zdCBTeW1ib2xQb2x5ZmlsbCA9IHR5cGVvZiBTeW1ib2wgPT09ICdmdW5jdGlvbicgJiYgdHlwZW9mIFN5bWJvbC5pdGVyYXRvciA9PT0gJ3N5bWJvbCcgP1xuICAgIFN5bWJvbCA6XG4gICAgZGVzY3JpcHRpb24gPT4gYFN5bWJvbCgke2Rlc2NyaXB0aW9ufSlgO1xuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJkb21cIiAvPlxuZnVuY3Rpb24gbm9vcCgpIHtcbiAgICAvLyBkbyBub3RoaW5nXG59XG5mdW5jdGlvbiBnZXRHbG9iYWxzKCkge1xuICAgIGlmICh0eXBlb2Ygc2VsZiAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAgICAgcmV0dXJuIHNlbGY7XG4gICAgfVxuICAgIGVsc2UgaWYgKHR5cGVvZiB3aW5kb3cgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHJldHVybiB3aW5kb3c7XG4gICAgfVxuICAgIGVsc2UgaWYgKHR5cGVvZiBnbG9iYWwgIT09ICd1bmRlZmluZWQnKSB7XG4gICAgICAgIHJldHVybiBnbG9iYWw7XG4gICAgfVxuICAgIHJldHVybiB1bmRlZmluZWQ7XG59XG5jb25zdCBnbG9iYWxzID0gZ2V0R2xvYmFscygpO1xuXG5mdW5jdGlvbiB0eXBlSXNPYmplY3QoeCkge1xuICAgIHJldHVybiAodHlwZW9mIHggPT09ICdvYmplY3QnICYmIHggIT09IG51bGwpIHx8IHR5cGVvZiB4ID09PSAnZnVuY3Rpb24nO1xufVxuY29uc3QgcmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uID0gIG5vb3A7XG5cbmNvbnN0IG9yaWdpbmFsUHJvbWlzZSA9IFByb21pc2U7XG5jb25zdCBvcmlnaW5hbFByb21pc2VUaGVuID0gUHJvbWlzZS5wcm90b3R5cGUudGhlbjtcbmNvbnN0IG9yaWdpbmFsUHJvbWlzZVJlc29sdmUgPSBQcm9taXNlLnJlc29sdmUuYmluZChvcmlnaW5hbFByb21pc2UpO1xuY29uc3Qgb3JpZ2luYWxQcm9taXNlUmVqZWN0ID0gUHJvbWlzZS5yZWplY3QuYmluZChvcmlnaW5hbFByb21pc2UpO1xuZnVuY3Rpb24gbmV3UHJvbWlzZShleGVjdXRvcikge1xuICAgIHJldHVybiBuZXcgb3JpZ2luYWxQcm9taXNlKGV4ZWN1dG9yKTtcbn1cbmZ1bmN0aW9uIHByb21pc2VSZXNvbHZlZFdpdGgodmFsdWUpIHtcbiAgICByZXR1cm4gb3JpZ2luYWxQcm9taXNlUmVzb2x2ZSh2YWx1ZSk7XG59XG5mdW5jdGlvbiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYXNvbikge1xuICAgIHJldHVybiBvcmlnaW5hbFByb21pc2VSZWplY3QocmVhc29uKTtcbn1cbmZ1bmN0aW9uIFBlcmZvcm1Qcm9taXNlVGhlbihwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCkge1xuICAgIC8vIFRoZXJlIGRvZXNuJ3QgYXBwZWFyIHRvIGJlIGFueSB3YXkgdG8gY29ycmVjdGx5IGVtdWxhdGUgdGhlIGJlaGF2aW91ciBmcm9tIEphdmFTY3JpcHQsIHNvIHRoaXMgaXMganVzdCBhblxuICAgIC8vIGFwcHJveGltYXRpb24uXG4gICAgcmV0dXJuIG9yaWdpbmFsUHJvbWlzZVRoZW4uY2FsbChwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCk7XG59XG5mdW5jdGlvbiB1cG9uUHJvbWlzZShwcm9taXNlLCBvbkZ1bGZpbGxlZCwgb25SZWplY3RlZCkge1xuICAgIFBlcmZvcm1Qcm9taXNlVGhlbihQZXJmb3JtUHJvbWlzZVRoZW4ocHJvbWlzZSwgb25GdWxmaWxsZWQsIG9uUmVqZWN0ZWQpLCB1bmRlZmluZWQsIHJldGhyb3dBc3NlcnRpb25FcnJvclJlamVjdGlvbik7XG59XG5mdW5jdGlvbiB1cG9uRnVsZmlsbG1lbnQocHJvbWlzZSwgb25GdWxmaWxsZWQpIHtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlLCBvbkZ1bGZpbGxlZCk7XG59XG5mdW5jdGlvbiB1cG9uUmVqZWN0aW9uKHByb21pc2UsIG9uUmVqZWN0ZWQpIHtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlLCB1bmRlZmluZWQsIG9uUmVqZWN0ZWQpO1xufVxuZnVuY3Rpb24gdHJhbnNmb3JtUHJvbWlzZVdpdGgocHJvbWlzZSwgZnVsZmlsbG1lbnRIYW5kbGVyLCByZWplY3Rpb25IYW5kbGVyKSB7XG4gICAgcmV0dXJuIFBlcmZvcm1Qcm9taXNlVGhlbihwcm9taXNlLCBmdWxmaWxsbWVudEhhbmRsZXIsIHJlamVjdGlvbkhhbmRsZXIpO1xufVxuZnVuY3Rpb24gc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZShwcm9taXNlKSB7XG4gICAgUGVyZm9ybVByb21pc2VUaGVuKHByb21pc2UsIHVuZGVmaW5lZCwgcmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uKTtcbn1cbmNvbnN0IHF1ZXVlTWljcm90YXNrID0gKCgpID0+IHtcbiAgICBjb25zdCBnbG9iYWxRdWV1ZU1pY3JvdGFzayA9IGdsb2JhbHMgJiYgZ2xvYmFscy5xdWV1ZU1pY3JvdGFzaztcbiAgICBpZiAodHlwZW9mIGdsb2JhbFF1ZXVlTWljcm90YXNrID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIHJldHVybiBnbG9iYWxRdWV1ZU1pY3JvdGFzaztcbiAgICB9XG4gICAgY29uc3QgcmVzb2x2ZWRQcm9taXNlID0gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIHJldHVybiAoZm4pID0+IFBlcmZvcm1Qcm9taXNlVGhlbihyZXNvbHZlZFByb21pc2UsIGZuKTtcbn0pKCk7XG5mdW5jdGlvbiByZWZsZWN0Q2FsbChGLCBWLCBhcmdzKSB7XG4gICAgaWYgKHR5cGVvZiBGICE9PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0FyZ3VtZW50IGlzIG5vdCBhIGZ1bmN0aW9uJyk7XG4gICAgfVxuICAgIHJldHVybiBGdW5jdGlvbi5wcm90b3R5cGUuYXBwbHkuY2FsbChGLCBWLCBhcmdzKTtcbn1cbmZ1bmN0aW9uIHByb21pc2VDYWxsKEYsIFYsIGFyZ3MpIHtcbiAgICB0cnkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aChyZWZsZWN0Q2FsbChGLCBWLCBhcmdzKSk7XG4gICAgfVxuICAgIGNhdGNoICh2YWx1ZSkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aCh2YWx1ZSk7XG4gICAgfVxufVxuXG4vLyBPcmlnaW5hbCBmcm9tIENocm9taXVtXG4vLyBodHRwczovL2Nocm9taXVtLmdvb2dsZXNvdXJjZS5jb20vY2hyb21pdW0vc3JjLysvMGFlZTQ0MzRhNGRiYTQyYTQyYWJhZWE5YmZiYzBjZDE5NmE2M2JjMS90aGlyZF9wYXJ0eS9ibGluay9yZW5kZXJlci9jb3JlL3N0cmVhbXMvU2ltcGxlUXVldWUuanNcbmNvbnN0IFFVRVVFX01BWF9BUlJBWV9TSVpFID0gMTYzODQ7XG4vKipcbiAqIFNpbXBsZSBxdWV1ZSBzdHJ1Y3R1cmUuXG4gKlxuICogQXZvaWRzIHNjYWxhYmlsaXR5IGlzc3VlcyB3aXRoIHVzaW5nIGEgcGFja2VkIGFycmF5IGRpcmVjdGx5IGJ5IHVzaW5nXG4gKiBtdWx0aXBsZSBhcnJheXMgaW4gYSBsaW5rZWQgbGlzdCBhbmQga2VlcGluZyB0aGUgYXJyYXkgc2l6ZSBib3VuZGVkLlxuICovXG5jbGFzcyBTaW1wbGVRdWV1ZSB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRoaXMuX2N1cnNvciA9IDA7XG4gICAgICAgIHRoaXMuX3NpemUgPSAwO1xuICAgICAgICAvLyBfZnJvbnQgYW5kIF9iYWNrIGFyZSBhbHdheXMgZGVmaW5lZC5cbiAgICAgICAgdGhpcy5fZnJvbnQgPSB7XG4gICAgICAgICAgICBfZWxlbWVudHM6IFtdLFxuICAgICAgICAgICAgX25leHQ6IHVuZGVmaW5lZFxuICAgICAgICB9O1xuICAgICAgICB0aGlzLl9iYWNrID0gdGhpcy5fZnJvbnQ7XG4gICAgICAgIC8vIFRoZSBjdXJzb3IgaXMgdXNlZCB0byBhdm9pZCBjYWxsaW5nIEFycmF5LnNoaWZ0KCkuXG4gICAgICAgIC8vIEl0IGNvbnRhaW5zIHRoZSBpbmRleCBvZiB0aGUgZnJvbnQgZWxlbWVudCBvZiB0aGUgYXJyYXkgaW5zaWRlIHRoZVxuICAgICAgICAvLyBmcm9udC1tb3N0IG5vZGUuIEl0IGlzIGFsd2F5cyBpbiB0aGUgcmFuZ2UgWzAsIFFVRVVFX01BWF9BUlJBWV9TSVpFKS5cbiAgICAgICAgdGhpcy5fY3Vyc29yID0gMDtcbiAgICAgICAgLy8gV2hlbiB0aGVyZSBpcyBvbmx5IG9uZSBub2RlLCBzaXplID09PSBlbGVtZW50cy5sZW5ndGggLSBjdXJzb3IuXG4gICAgICAgIHRoaXMuX3NpemUgPSAwO1xuICAgIH1cbiAgICBnZXQgbGVuZ3RoKCkge1xuICAgICAgICByZXR1cm4gdGhpcy5fc2l6ZTtcbiAgICB9XG4gICAgLy8gRm9yIGV4Y2VwdGlvbiBzYWZldHksIHRoaXMgbWV0aG9kIGlzIHN0cnVjdHVyZWQgaW4gb3JkZXI6XG4gICAgLy8gMS4gUmVhZCBzdGF0ZVxuICAgIC8vIDIuIENhbGN1bGF0ZSByZXF1aXJlZCBzdGF0ZSBtdXRhdGlvbnNcbiAgICAvLyAzLiBQZXJmb3JtIHN0YXRlIG11dGF0aW9uc1xuICAgIHB1c2goZWxlbWVudCkge1xuICAgICAgICBjb25zdCBvbGRCYWNrID0gdGhpcy5fYmFjaztcbiAgICAgICAgbGV0IG5ld0JhY2sgPSBvbGRCYWNrO1xuICAgICAgICBpZiAob2xkQmFjay5fZWxlbWVudHMubGVuZ3RoID09PSBRVUVVRV9NQVhfQVJSQVlfU0laRSAtIDEpIHtcbiAgICAgICAgICAgIG5ld0JhY2sgPSB7XG4gICAgICAgICAgICAgICAgX2VsZW1lbnRzOiBbXSxcbiAgICAgICAgICAgICAgICBfbmV4dDogdW5kZWZpbmVkXG4gICAgICAgICAgICB9O1xuICAgICAgICB9XG4gICAgICAgIC8vIHB1c2goKSBpcyB0aGUgbXV0YXRpb24gbW9zdCBsaWtlbHkgdG8gdGhyb3cgYW4gZXhjZXB0aW9uLCBzbyBpdFxuICAgICAgICAvLyBnb2VzIGZpcnN0LlxuICAgICAgICBvbGRCYWNrLl9lbGVtZW50cy5wdXNoKGVsZW1lbnQpO1xuICAgICAgICBpZiAobmV3QmFjayAhPT0gb2xkQmFjaykge1xuICAgICAgICAgICAgdGhpcy5fYmFjayA9IG5ld0JhY2s7XG4gICAgICAgICAgICBvbGRCYWNrLl9uZXh0ID0gbmV3QmFjaztcbiAgICAgICAgfVxuICAgICAgICArK3RoaXMuX3NpemU7XG4gICAgfVxuICAgIC8vIExpa2UgcHVzaCgpLCBzaGlmdCgpIGZvbGxvd3MgdGhlIHJlYWQgLT4gY2FsY3VsYXRlIC0+IG11dGF0ZSBwYXR0ZXJuIGZvclxuICAgIC8vIGV4Y2VwdGlvbiBzYWZldHkuXG4gICAgc2hpZnQoKSB7IC8vIG11c3Qgbm90IGJlIGNhbGxlZCBvbiBhbiBlbXB0eSBxdWV1ZVxuICAgICAgICBjb25zdCBvbGRGcm9udCA9IHRoaXMuX2Zyb250O1xuICAgICAgICBsZXQgbmV3RnJvbnQgPSBvbGRGcm9udDtcbiAgICAgICAgY29uc3Qgb2xkQ3Vyc29yID0gdGhpcy5fY3Vyc29yO1xuICAgICAgICBsZXQgbmV3Q3Vyc29yID0gb2xkQ3Vyc29yICsgMTtcbiAgICAgICAgY29uc3QgZWxlbWVudHMgPSBvbGRGcm9udC5fZWxlbWVudHM7XG4gICAgICAgIGNvbnN0IGVsZW1lbnQgPSBlbGVtZW50c1tvbGRDdXJzb3JdO1xuICAgICAgICBpZiAobmV3Q3Vyc29yID09PSBRVUVVRV9NQVhfQVJSQVlfU0laRSkge1xuICAgICAgICAgICAgbmV3RnJvbnQgPSBvbGRGcm9udC5fbmV4dDtcbiAgICAgICAgICAgIG5ld0N1cnNvciA9IDA7XG4gICAgICAgIH1cbiAgICAgICAgLy8gTm8gbXV0YXRpb25zIGJlZm9yZSB0aGlzIHBvaW50LlxuICAgICAgICAtLXRoaXMuX3NpemU7XG4gICAgICAgIHRoaXMuX2N1cnNvciA9IG5ld0N1cnNvcjtcbiAgICAgICAgaWYgKG9sZEZyb250ICE9PSBuZXdGcm9udCkge1xuICAgICAgICAgICAgdGhpcy5fZnJvbnQgPSBuZXdGcm9udDtcbiAgICAgICAgfVxuICAgICAgICAvLyBQZXJtaXQgc2hpZnRlZCBlbGVtZW50IHRvIGJlIGdhcmJhZ2UgY29sbGVjdGVkLlxuICAgICAgICBlbGVtZW50c1tvbGRDdXJzb3JdID0gdW5kZWZpbmVkO1xuICAgICAgICByZXR1cm4gZWxlbWVudDtcbiAgICB9XG4gICAgLy8gVGhlIHRyaWNreSB0aGluZyBhYm91dCBmb3JFYWNoKCkgaXMgdGhhdCBpdCBjYW4gYmUgY2FsbGVkXG4gICAgLy8gcmUtZW50cmFudGx5LiBUaGUgcXVldWUgbWF5IGJlIG11dGF0ZWQgaW5zaWRlIHRoZSBjYWxsYmFjay4gSXQgaXMgZWFzeSB0b1xuICAgIC8vIHNlZSB0aGF0IHB1c2goKSB3aXRoaW4gdGhlIGNhbGxiYWNrIGhhcyBubyBuZWdhdGl2ZSBlZmZlY3RzIHNpbmNlIHRoZSBlbmRcbiAgICAvLyBvZiB0aGUgcXVldWUgaXMgY2hlY2tlZCBmb3Igb24gZXZlcnkgaXRlcmF0aW9uLiBJZiBzaGlmdCgpIGlzIGNhbGxlZFxuICAgIC8vIHJlcGVhdGVkbHkgd2l0aGluIHRoZSBjYWxsYmFjayB0aGVuIHRoZSBuZXh0IGl0ZXJhdGlvbiBtYXkgcmV0dXJuIGFuXG4gICAgLy8gZWxlbWVudCB0aGF0IGhhcyBiZWVuIHJlbW92ZWQuIEluIHRoaXMgY2FzZSB0aGUgY2FsbGJhY2sgd2lsbCBiZSBjYWxsZWRcbiAgICAvLyB3aXRoIHVuZGVmaW5lZCB2YWx1ZXMgdW50aWwgd2UgZWl0aGVyIFwiY2F0Y2ggdXBcIiB3aXRoIGVsZW1lbnRzIHRoYXQgc3RpbGxcbiAgICAvLyBleGlzdCBvciByZWFjaCB0aGUgYmFjayBvZiB0aGUgcXVldWUuXG4gICAgZm9yRWFjaChjYWxsYmFjaykge1xuICAgICAgICBsZXQgaSA9IHRoaXMuX2N1cnNvcjtcbiAgICAgICAgbGV0IG5vZGUgPSB0aGlzLl9mcm9udDtcbiAgICAgICAgbGV0IGVsZW1lbnRzID0gbm9kZS5fZWxlbWVudHM7XG4gICAgICAgIHdoaWxlIChpICE9PSBlbGVtZW50cy5sZW5ndGggfHwgbm9kZS5fbmV4dCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBpZiAoaSA9PT0gZWxlbWVudHMubGVuZ3RoKSB7XG4gICAgICAgICAgICAgICAgbm9kZSA9IG5vZGUuX25leHQ7XG4gICAgICAgICAgICAgICAgZWxlbWVudHMgPSBub2RlLl9lbGVtZW50cztcbiAgICAgICAgICAgICAgICBpID0gMDtcbiAgICAgICAgICAgICAgICBpZiAoZWxlbWVudHMubGVuZ3RoID09PSAwKSB7XG4gICAgICAgICAgICAgICAgICAgIGJyZWFrO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNhbGxiYWNrKGVsZW1lbnRzW2ldKTtcbiAgICAgICAgICAgICsraTtcbiAgICAgICAgfVxuICAgIH1cbiAgICAvLyBSZXR1cm4gdGhlIGVsZW1lbnQgdGhhdCB3b3VsZCBiZSByZXR1cm5lZCBpZiBzaGlmdCgpIHdhcyBjYWxsZWQgbm93LFxuICAgIC8vIHdpdGhvdXQgbW9kaWZ5aW5nIHRoZSBxdWV1ZS5cbiAgICBwZWVrKCkgeyAvLyBtdXN0IG5vdCBiZSBjYWxsZWQgb24gYW4gZW1wdHkgcXVldWVcbiAgICAgICAgY29uc3QgZnJvbnQgPSB0aGlzLl9mcm9udDtcbiAgICAgICAgY29uc3QgY3Vyc29yID0gdGhpcy5fY3Vyc29yO1xuICAgICAgICByZXR1cm4gZnJvbnQuX2VsZW1lbnRzW2N1cnNvcl07XG4gICAgfVxufVxuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNJbml0aWFsaXplKHJlYWRlciwgc3RyZWFtKSB7XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgIHN0cmVhbS5fcmVhZGVyID0gcmVhZGVyO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlID09PSAncmVhZGFibGUnKSB7XG4gICAgICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpO1xuICAgIH1cbiAgICBlbHNlIGlmIChzdHJlYW0uX3N0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHJlYWRlcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHJlYWRlciwgc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxufVxuLy8gQSBjbGllbnQgb2YgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIGFuZCBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIgbWF5IHVzZSB0aGVzZSBmdW5jdGlvbnMgZGlyZWN0bHkgdG8gYnlwYXNzIHN0YXRlXG4vLyBjaGVjay5cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY0NhbmNlbChyZWFkZXIsIHJlYXNvbikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHJlYWRlci5fb3duZXJSZWFkYWJsZVN0cmVhbTtcbiAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1DYW5jZWwoc3RyZWFtLCByZWFzb24pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpIHtcbiAgICBpZiAocmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtLl9zdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIG5ldyBUeXBlRXJyb3IoYFJlYWRlciB3YXMgcmVsZWFzZWQgYW5kIGNhbiBubyBsb25nZXIgYmUgdXNlZCB0byBtb25pdG9yIHRoZSBzdHJlYW0ncyBjbG9zZWRuZXNzYCkpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNldFRvUmVqZWN0ZWQocmVhZGVyLCBuZXcgVHlwZUVycm9yKGBSZWFkZXIgd2FzIHJlbGVhc2VkIGFuZCBjYW4gbm8gbG9uZ2VyIGJlIHVzZWQgdG8gbW9uaXRvciB0aGUgc3RyZWFtJ3MgY2xvc2VkbmVzc2ApKTtcbiAgICB9XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtLl9yZWFkZXIgPSB1bmRlZmluZWQ7XG4gICAgcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID0gdW5kZWZpbmVkO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIHJlYWRlcnMuXG5mdW5jdGlvbiByZWFkZXJMb2NrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcignQ2Fubm90ICcgKyBuYW1lICsgJyBhIHN0cmVhbSB1c2luZyBhIHJlbGVhc2VkIHJlYWRlcicpO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpIHtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgICAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gcmVqZWN0O1xuICAgIH0pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZChyZWFkZXIsIHJlYXNvbikge1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZShyZWFkZXIpO1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVqZWN0KHJlYWRlciwgcmVhc29uKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVzb2x2ZWQocmVhZGVyKSB7XG4gICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VJbml0aWFsaXplKHJlYWRlcik7XG4gICAgZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNvbHZlKHJlYWRlcik7XG59XG5mdW5jdGlvbiBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIHJlYXNvbikge1xuICAgIGlmIChyZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBzZXRQcm9taXNlSXNIYW5kbGVkVG9UcnVlKHJlYWRlci5fY2xvc2VkUHJvbWlzZSk7XG4gICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3JlamVjdChyZWFzb24pO1xuICAgIHJlYWRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHJlYWRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlc2V0VG9SZWplY3RlZChyZWFkZXIsIHJlYXNvbikge1xuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQocmVhZGVyLCByZWFzb24pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFJlYWRlckNsb3NlZFByb21pc2VSZXNvbHZlKHJlYWRlcikge1xuICAgIGlmIChyZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgcmVhZGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICByZWFkZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gdW5kZWZpbmVkO1xufVxuXG5jb25zdCBBYm9ydFN0ZXBzID0gU3ltYm9sUG9seWZpbGwoJ1tbQWJvcnRTdGVwc11dJyk7XG5jb25zdCBFcnJvclN0ZXBzID0gU3ltYm9sUG9seWZpbGwoJ1tbRXJyb3JTdGVwc11dJyk7XG5jb25zdCBDYW5jZWxTdGVwcyA9IFN5bWJvbFBvbHlmaWxsKCdbW0NhbmNlbFN0ZXBzXV0nKTtcbmNvbnN0IFB1bGxTdGVwcyA9IFN5bWJvbFBvbHlmaWxsKCdbW1B1bGxTdGVwc11dJyk7XG5cbi8vLyA8cmVmZXJlbmNlIGxpYj1cImVzMjAxNS5jb3JlXCIgLz5cbi8vIGh0dHBzOi8vZGV2ZWxvcGVyLm1vemlsbGEub3JnL2VuLVVTL2RvY3MvV2ViL0phdmFTY3JpcHQvUmVmZXJlbmNlL0dsb2JhbF9PYmplY3RzL051bWJlci9pc0Zpbml0ZSNQb2x5ZmlsbFxuY29uc3QgTnVtYmVySXNGaW5pdGUgPSBOdW1iZXIuaXNGaW5pdGUgfHwgZnVuY3Rpb24gKHgpIHtcbiAgICByZXR1cm4gdHlwZW9mIHggPT09ICdudW1iZXInICYmIGlzRmluaXRlKHgpO1xufTtcblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LmNvcmVcIiAvPlxuLy8gaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvSmF2YVNjcmlwdC9SZWZlcmVuY2UvR2xvYmFsX09iamVjdHMvTWF0aC90cnVuYyNQb2x5ZmlsbFxuY29uc3QgTWF0aFRydW5jID0gTWF0aC50cnVuYyB8fCBmdW5jdGlvbiAodikge1xuICAgIHJldHVybiB2IDwgMCA/IE1hdGguY2VpbCh2KSA6IE1hdGguZmxvb3Iodik7XG59O1xuXG4vLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNpZGwtZGljdGlvbmFyaWVzXG5mdW5jdGlvbiBpc0RpY3Rpb25hcnkoeCkge1xuICAgIHJldHVybiB0eXBlb2YgeCA9PT0gJ29iamVjdCcgfHwgdHlwZW9mIHggPT09ICdmdW5jdGlvbic7XG59XG5mdW5jdGlvbiBhc3NlcnREaWN0aW9uYXJ5KG9iaiwgY29udGV4dCkge1xuICAgIGlmIChvYmogIT09IHVuZGVmaW5lZCAmJiAhaXNEaWN0aW9uYXJ5KG9iaikpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYW4gb2JqZWN0LmApO1xuICAgIH1cbn1cbi8vIGh0dHBzOi8vaGV5Y2FtLmdpdGh1Yi5pby93ZWJpZGwvI2lkbC1jYWxsYmFjay1mdW5jdGlvbnNcbmZ1bmN0aW9uIGFzc2VydEZ1bmN0aW9uKHgsIGNvbnRleHQpIHtcbiAgICBpZiAodHlwZW9mIHggIT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYSBmdW5jdGlvbi5gKTtcbiAgICB9XG59XG4vLyBodHRwczovL2hleWNhbS5naXRodWIuaW8vd2ViaWRsLyNpZGwtb2JqZWN0XG5mdW5jdGlvbiBpc09iamVjdCh4KSB7XG4gICAgcmV0dXJuICh0eXBlb2YgeCA9PT0gJ29iamVjdCcgJiYgeCAhPT0gbnVsbCkgfHwgdHlwZW9mIHggPT09ICdmdW5jdGlvbic7XG59XG5mdW5jdGlvbiBhc3NlcnRPYmplY3QoeCwgY29udGV4dCkge1xuICAgIGlmICghaXNPYmplY3QoeCkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYW4gb2JqZWN0LmApO1xuICAgIH1cbn1cbmZ1bmN0aW9uIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoeCwgcG9zaXRpb24sIGNvbnRleHQpIHtcbiAgICBpZiAoeCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYFBhcmFtZXRlciAke3Bvc2l0aW9ufSBpcyByZXF1aXJlZCBpbiAnJHtjb250ZXh0fScuYCk7XG4gICAgfVxufVxuZnVuY3Rpb24gYXNzZXJ0UmVxdWlyZWRGaWVsZCh4LCBmaWVsZCwgY29udGV4dCkge1xuICAgIGlmICh4ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtmaWVsZH0gaXMgcmVxdWlyZWQgaW4gJyR7Y29udGV4dH0nLmApO1xuICAgIH1cbn1cbi8vIGh0dHBzOi8vaGV5Y2FtLmdpdGh1Yi5pby93ZWJpZGwvI2lkbC11bnJlc3RyaWN0ZWQtZG91YmxlXG5mdW5jdGlvbiBjb252ZXJ0VW5yZXN0cmljdGVkRG91YmxlKHZhbHVlKSB7XG4gICAgcmV0dXJuIE51bWJlcih2YWx1ZSk7XG59XG5mdW5jdGlvbiBjZW5zb3JOZWdhdGl2ZVplcm8oeCkge1xuICAgIHJldHVybiB4ID09PSAwID8gMCA6IHg7XG59XG5mdW5jdGlvbiBpbnRlZ2VyUGFydCh4KSB7XG4gICAgcmV0dXJuIGNlbnNvck5lZ2F0aXZlWmVybyhNYXRoVHJ1bmMoeCkpO1xufVxuLy8gaHR0cHM6Ly9oZXljYW0uZ2l0aHViLmlvL3dlYmlkbC8jaWRsLXVuc2lnbmVkLWxvbmctbG9uZ1xuZnVuY3Rpb24gY29udmVydFVuc2lnbmVkTG9uZ0xvbmdXaXRoRW5mb3JjZVJhbmdlKHZhbHVlLCBjb250ZXh0KSB7XG4gICAgY29uc3QgbG93ZXJCb3VuZCA9IDA7XG4gICAgY29uc3QgdXBwZXJCb3VuZCA9IE51bWJlci5NQVhfU0FGRV9JTlRFR0VSO1xuICAgIGxldCB4ID0gTnVtYmVyKHZhbHVlKTtcbiAgICB4ID0gY2Vuc29yTmVnYXRpdmVaZXJvKHgpO1xuICAgIGlmICghTnVtYmVySXNGaW5pdGUoeCkpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBub3QgYSBmaW5pdGUgbnVtYmVyYCk7XG4gICAgfVxuICAgIHggPSBpbnRlZ2VyUGFydCh4KTtcbiAgICBpZiAoeCA8IGxvd2VyQm91bmQgfHwgeCA+IHVwcGVyQm91bmQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSBpcyBvdXRzaWRlIHRoZSBhY2NlcHRlZCByYW5nZSBvZiAke2xvd2VyQm91bmR9IHRvICR7dXBwZXJCb3VuZH0sIGluY2x1c2l2ZWApO1xuICAgIH1cbiAgICBpZiAoIU51bWJlcklzRmluaXRlKHgpIHx8IHggPT09IDApIHtcbiAgICAgICAgcmV0dXJuIDA7XG4gICAgfVxuICAgIC8vIFRPRE8gVXNlIEJpZ0ludCBpZiBzdXBwb3J0ZWQ/XG4gICAgLy8gbGV0IHhCaWdJbnQgPSBCaWdJbnQoaW50ZWdlclBhcnQoeCkpO1xuICAgIC8vIHhCaWdJbnQgPSBCaWdJbnQuYXNVaW50Tig2NCwgeEJpZ0ludCk7XG4gICAgLy8gcmV0dXJuIE51bWJlcih4QmlnSW50KTtcbiAgICByZXR1cm4geDtcbn1cblxuZnVuY3Rpb24gYXNzZXJ0UmVhZGFibGVTdHJlYW0oeCwgY29udGV4dCkge1xuICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbSh4KSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhIFJlYWRhYmxlU3RyZWFtLmApO1xuICAgIH1cbn1cblxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gQWNxdWlyZVJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihzdHJlYW0pIHtcbiAgICByZXR1cm4gbmV3IFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihzdHJlYW0pO1xufVxuLy8gUmVhZGFibGVTdHJlYW0gQVBJIGV4cG9zZWQgZm9yIGNvbnRyb2xsZXJzLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdChzdHJlYW0sIHJlYWRSZXF1ZXN0KSB7XG4gICAgc3RyZWFtLl9yZWFkZXIuX3JlYWRSZXF1ZXN0cy5wdXNoKHJlYWRSZXF1ZXN0KTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgY2h1bmssIGRvbmUpIHtcbiAgICBjb25zdCByZWFkZXIgPSBzdHJlYW0uX3JlYWRlcjtcbiAgICBjb25zdCByZWFkUmVxdWVzdCA9IHJlYWRlci5fcmVhZFJlcXVlc3RzLnNoaWZ0KCk7XG4gICAgaWYgKGRvbmUpIHtcbiAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIHJlYWRSZXF1ZXN0Ll9jaHVua1N0ZXBzKGNodW5rKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pIHtcbiAgICByZXR1cm4gc3RyZWFtLl9yZWFkZXIuX3JlYWRSZXF1ZXN0cy5sZW5ndGg7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIoc3RyZWFtKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgaWYgKHJlYWRlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG4vKipcbiAqIEEgZGVmYXVsdCByZWFkZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyJyk7XG4gICAgICAgIGFzc2VydFJlYWRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHJlYWRpbmcgYnkgYW5vdGhlciByZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNJbml0aWFsaXplKHRoaXMsIHN0cmVhbSk7XG4gICAgICAgIHRoaXMuX3JlYWRSZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIHN0cmVhbSBiZWNvbWVzIGNsb3NlZCxcbiAgICAgKiBvciByZWplY3RlZCBpZiB0aGUgc3RyZWFtIGV2ZXIgZXJyb3JzIG9yIHRoZSByZWFkZXIncyBsb2NrIGlzIHJlbGVhc2VkIGJlZm9yZSB0aGUgc3RyZWFtIGZpbmlzaGVzIGNsb3NpbmcuXG4gICAgICovXG4gICAgZ2V0IGNsb3NlZCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Nsb3NlZCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5fY2xvc2VkUHJvbWlzZTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogSWYgdGhlIHJlYWRlciBpcyBhY3RpdmUsIGJlaGF2ZXMgdGhlIHNhbWUgYXMge0BsaW5rIFJlYWRhYmxlU3RyZWFtLmNhbmNlbCB8IHN0cmVhbS5jYW5jZWwocmVhc29uKX0uXG4gICAgICovXG4gICAgY2FuY2VsKHJlYXNvbiA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lclJlYWRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYWRlckxvY2tFeGNlcHRpb24oJ2NhbmNlbCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgYSBwcm9taXNlIHRoYXQgYWxsb3dzIGFjY2VzcyB0byB0aGUgbmV4dCBjaHVuayBmcm9tIHRoZSBzdHJlYW0ncyBpbnRlcm5hbCBxdWV1ZSwgaWYgYXZhaWxhYmxlLlxuICAgICAqXG4gICAgICogSWYgcmVhZGluZyBhIGNodW5rIGNhdXNlcyB0aGUgcXVldWUgdG8gYmVjb21lIGVtcHR5LCBtb3JlIGRhdGEgd2lsbCBiZSBwdWxsZWQgZnJvbSB0aGUgdW5kZXJseWluZyBzb3VyY2UuXG4gICAgICovXG4gICAgcmVhZCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlYWQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyUmVhZGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgocmVhZGVyTG9ja0V4Y2VwdGlvbigncmVhZCBmcm9tJykpO1xuICAgICAgICB9XG4gICAgICAgIGxldCByZXNvbHZlUHJvbWlzZTtcbiAgICAgICAgbGV0IHJlamVjdFByb21pc2U7XG4gICAgICAgIGNvbnN0IHByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgICAgIHJlc29sdmVQcm9taXNlID0gcmVzb2x2ZTtcbiAgICAgICAgICAgIHJlamVjdFByb21pc2UgPSByZWplY3Q7XG4gICAgICAgIH0pO1xuICAgICAgICBjb25zdCByZWFkUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9jaHVua1N0ZXBzOiBjaHVuayA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSksXG4gICAgICAgICAgICBfY2xvc2VTdGVwczogKCkgPT4gcmVzb2x2ZVByb21pc2UoeyB2YWx1ZTogdW5kZWZpbmVkLCBkb25lOiB0cnVlIH0pLFxuICAgICAgICAgICAgX2Vycm9yU3RlcHM6IGUgPT4gcmVqZWN0UHJvbWlzZShlKVxuICAgICAgICB9O1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHRoaXMsIHJlYWRSZXF1ZXN0KTtcbiAgICAgICAgcmV0dXJuIHByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJlbGVhc2VzIHRoZSByZWFkZXIncyBsb2NrIG9uIHRoZSBjb3JyZXNwb25kaW5nIHN0cmVhbS4gQWZ0ZXIgdGhlIGxvY2sgaXMgcmVsZWFzZWQsIHRoZSByZWFkZXIgaXMgbm8gbG9uZ2VyIGFjdGl2ZS5cbiAgICAgKiBJZiB0aGUgYXNzb2NpYXRlZCBzdHJlYW0gaXMgZXJyb3JlZCB3aGVuIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmVhZGVyIHdpbGwgYXBwZWFyIGVycm9yZWQgaW4gdGhlIHNhbWUgd2F5XG4gICAgICogZnJvbSBub3cgb247IG90aGVyd2lzZSwgdGhlIHJlYWRlciB3aWxsIGFwcGVhciBjbG9zZWQuXG4gICAgICpcbiAgICAgKiBBIHJlYWRlcidzIGxvY2sgY2Fubm90IGJlIHJlbGVhc2VkIHdoaWxlIGl0IHN0aWxsIGhhcyBhIHBlbmRpbmcgcmVhZCByZXF1ZXN0LCBpLmUuLCBpZiBhIHByb21pc2UgcmV0dXJuZWQgYnlcbiAgICAgKiB0aGUgcmVhZGVyJ3Mge0BsaW5rIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5yZWFkIHwgcmVhZCgpfSBtZXRob2QgaGFzIG5vdCB5ZXQgYmVlbiBzZXR0bGVkLiBBdHRlbXB0aW5nIHRvXG4gICAgICogZG8gc28gd2lsbCB0aHJvdyBhIGBUeXBlRXJyb3JgIGFuZCBsZWF2ZSB0aGUgcmVhZGVyIGxvY2tlZCB0byB0aGUgc3RyZWFtLlxuICAgICAqL1xuICAgIHJlbGVhc2VMb2NrKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0UmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbigncmVsZWFzZUxvY2snKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX3JlYWRSZXF1ZXN0cy5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUcmllZCB0byByZWxlYXNlIGEgcmVhZGVyIGxvY2sgd2hlbiB0aGF0IHJlYWRlciBoYXMgcGVuZGluZyByZWFkKCkgY2FsbHMgdW4tc2V0dGxlZCcpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY1JlbGVhc2UodGhpcyk7XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZSwge1xuICAgIGNhbmNlbDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVhZDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVsZWFzZUxvY2s6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGNsb3NlZDogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXInLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEFic3RyYWN0IG9wZXJhdGlvbnMgZm9yIHRoZSByZWFkZXJzLlxuZnVuY3Rpb24gSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ19yZWFkUmVxdWVzdHMnKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyUmVhZChyZWFkZXIsIHJlYWRSZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gdHJ1ZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICB9XG4gICAgZWxzZSBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJlYWRSZXF1ZXN0Ll9lcnJvclN0ZXBzKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgc3RyZWFtLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXJbUHVsbFN0ZXBzXShyZWFkUmVxdWVzdCk7XG4gICAgfVxufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlci5cbmZ1bmN0aW9uIGRlZmF1bHRSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyLnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJgKTtcbn1cblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE4LmFzeW5jaXRlcmFibGVcIiAvPlxuLyogZXNsaW50LWRpc2FibGUgQHR5cGVzY3JpcHQtZXNsaW50L25vLWVtcHR5LWZ1bmN0aW9uICovXG5jb25zdCBBc3luY0l0ZXJhdG9yUHJvdG90eXBlID0gT2JqZWN0LmdldFByb3RvdHlwZU9mKE9iamVjdC5nZXRQcm90b3R5cGVPZihhc3luYyBmdW5jdGlvbiogKCkgeyB9KS5wcm90b3R5cGUpO1xuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJlczIwMTguYXN5bmNpdGVyYWJsZVwiIC8+XG5jbGFzcyBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3JJbXBsIHtcbiAgICBjb25zdHJ1Y3RvcihyZWFkZXIsIHByZXZlbnRDYW5jZWwpIHtcbiAgICAgICAgdGhpcy5fb25nb2luZ1Byb21pc2UgPSB1bmRlZmluZWQ7XG4gICAgICAgIHRoaXMuX2lzRmluaXNoZWQgPSBmYWxzZTtcbiAgICAgICAgdGhpcy5fcmVhZGVyID0gcmVhZGVyO1xuICAgICAgICB0aGlzLl9wcmV2ZW50Q2FuY2VsID0gcHJldmVudENhbmNlbDtcbiAgICB9XG4gICAgbmV4dCgpIHtcbiAgICAgICAgY29uc3QgbmV4dFN0ZXBzID0gKCkgPT4gdGhpcy5fbmV4dFN0ZXBzKCk7XG4gICAgICAgIHRoaXMuX29uZ29pbmdQcm9taXNlID0gdGhpcy5fb25nb2luZ1Byb21pc2UgP1xuICAgICAgICAgICAgdHJhbnNmb3JtUHJvbWlzZVdpdGgodGhpcy5fb25nb2luZ1Byb21pc2UsIG5leHRTdGVwcywgbmV4dFN0ZXBzKSA6XG4gICAgICAgICAgICBuZXh0U3RlcHMoKTtcbiAgICAgICAgcmV0dXJuIHRoaXMuX29uZ29pbmdQcm9taXNlO1xuICAgIH1cbiAgICByZXR1cm4odmFsdWUpIHtcbiAgICAgICAgY29uc3QgcmV0dXJuU3RlcHMgPSAoKSA9PiB0aGlzLl9yZXR1cm5TdGVwcyh2YWx1ZSk7XG4gICAgICAgIHJldHVybiB0aGlzLl9vbmdvaW5nUHJvbWlzZSA/XG4gICAgICAgICAgICB0cmFuc2Zvcm1Qcm9taXNlV2l0aCh0aGlzLl9vbmdvaW5nUHJvbWlzZSwgcmV0dXJuU3RlcHMsIHJldHVyblN0ZXBzKSA6XG4gICAgICAgICAgICByZXR1cm5TdGVwcygpO1xuICAgIH1cbiAgICBfbmV4dFN0ZXBzKCkge1xuICAgICAgICBpZiAodGhpcy5faXNGaW5pc2hlZCkge1xuICAgICAgICAgICAgcmV0dXJuIFByb21pc2UucmVzb2x2ZSh7IHZhbHVlOiB1bmRlZmluZWQsIGRvbmU6IHRydWUgfSk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcmVhZGVyID0gdGhpcy5fcmVhZGVyO1xuICAgICAgICBpZiAocmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHJlYWRlckxvY2tFeGNlcHRpb24oJ2l0ZXJhdGUnKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IHJlc29sdmVQcm9taXNlO1xuICAgICAgICBsZXQgcmVqZWN0UHJvbWlzZTtcbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICAgICAgcmVzb2x2ZVByb21pc2UgPSByZXNvbHZlO1xuICAgICAgICAgICAgcmVqZWN0UHJvbWlzZSA9IHJlamVjdDtcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IHJlYWRSZXF1ZXN0ID0ge1xuICAgICAgICAgICAgX2NodW5rU3RlcHM6IGNodW5rID0+IHtcbiAgICAgICAgICAgICAgICB0aGlzLl9vbmdvaW5nUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICAgICAgICAvLyBUaGlzIG5lZWRzIHRvIGJlIGRlbGF5ZWQgYnkgb25lIG1pY3JvdGFzaywgb3RoZXJ3aXNlIHdlIHN0b3AgcHVsbGluZyB0b28gZWFybHkgd2hpY2ggYnJlYWtzIGEgdGVzdC5cbiAgICAgICAgICAgICAgICAvLyBGSVhNRSBJcyB0aGlzIGEgYnVnIGluIHRoZSBzcGVjaWZpY2F0aW9uLCBvciBpbiB0aGUgdGVzdD9cbiAgICAgICAgICAgICAgICBxdWV1ZU1pY3JvdGFzaygoKSA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSkpO1xuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiB7XG4gICAgICAgICAgICAgICAgdGhpcy5fb25nb2luZ1Byb21pc2UgPSB1bmRlZmluZWQ7XG4gICAgICAgICAgICAgICAgdGhpcy5faXNGaW5pc2hlZCA9IHRydWU7XG4gICAgICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgICAgIHJlc29sdmVQcm9taXNlKHsgdmFsdWU6IHVuZGVmaW5lZCwgZG9uZTogdHJ1ZSB9KTtcbiAgICAgICAgICAgIH0sXG4gICAgICAgICAgICBfZXJyb3JTdGVwczogcmVhc29uID0+IHtcbiAgICAgICAgICAgICAgICB0aGlzLl9vbmdvaW5nUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICAgICAgICAgICAgICB0aGlzLl9pc0ZpbmlzaGVkID0gdHJ1ZTtcbiAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlKHJlYWRlcik7XG4gICAgICAgICAgICAgICAgcmVqZWN0UHJvbWlzZShyZWFzb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9O1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHJlYWRlciwgcmVhZFJlcXVlc3QpO1xuICAgICAgICByZXR1cm4gcHJvbWlzZTtcbiAgICB9XG4gICAgX3JldHVyblN0ZXBzKHZhbHVlKSB7XG4gICAgICAgIGlmICh0aGlzLl9pc0ZpbmlzaGVkKSB7XG4gICAgICAgICAgICByZXR1cm4gUHJvbWlzZS5yZXNvbHZlKHsgdmFsdWUsIGRvbmU6IHRydWUgfSk7XG4gICAgICAgIH1cbiAgICAgICAgdGhpcy5faXNGaW5pc2hlZCA9IHRydWU7XG4gICAgICAgIGNvbnN0IHJlYWRlciA9IHRoaXMuX3JlYWRlcjtcbiAgICAgICAgaWYgKHJlYWRlci5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChyZWFkZXJMb2NrRXhjZXB0aW9uKCdmaW5pc2ggaXRlcmF0aW5nJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghdGhpcy5fcHJldmVudENhbmNlbCkge1xuICAgICAgICAgICAgY29uc3QgcmVzdWx0ID0gUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljQ2FuY2VsKHJlYWRlciwgdmFsdWUpO1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKHJlc3VsdCwgKCkgPT4gKHsgdmFsdWUsIGRvbmU6IHRydWUgfSkpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtUmVhZGVyR2VuZXJpY1JlbGVhc2UocmVhZGVyKTtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgoeyB2YWx1ZSwgZG9uZTogdHJ1ZSB9KTtcbiAgICB9XG59XG5jb25zdCBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3JQcm90b3R5cGUgPSB7XG4gICAgbmV4dCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQXN5bmNJdGVyYXRvckJyYW5kQ2hlY2tFeGNlcHRpb24oJ25leHQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHRoaXMuX2FzeW5jSXRlcmF0b3JJbXBsLm5leHQoKTtcbiAgICB9LFxuICAgIHJldHVybih2YWx1ZSkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW1Bc3luY0l0ZXJhdG9yQnJhbmRDaGVja0V4Y2VwdGlvbigncmV0dXJuJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9hc3luY0l0ZXJhdG9ySW1wbC5yZXR1cm4odmFsdWUpO1xuICAgIH1cbn07XG5pZiAoQXN5bmNJdGVyYXRvclByb3RvdHlwZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgT2JqZWN0LnNldFByb3RvdHlwZU9mKFJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvclByb3RvdHlwZSwgQXN5bmNJdGVyYXRvclByb3RvdHlwZSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW0uXG5mdW5jdGlvbiBBY3F1aXJlUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHN0cmVhbSwgcHJldmVudENhbmNlbCkge1xuICAgIGNvbnN0IHJlYWRlciA9IEFjcXVpcmVSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoc3RyZWFtKTtcbiAgICBjb25zdCBpbXBsID0gbmV3IFJlYWRhYmxlU3RyZWFtQXN5bmNJdGVyYXRvckltcGwocmVhZGVyLCBwcmV2ZW50Q2FuY2VsKTtcbiAgICBjb25zdCBpdGVyYXRvciA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yUHJvdG90eXBlKTtcbiAgICBpdGVyYXRvci5fYXN5bmNJdGVyYXRvckltcGwgPSBpbXBsO1xuICAgIHJldHVybiBpdGVyYXRvcjtcbn1cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1Bc3luY0l0ZXJhdG9yKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfYXN5bmNJdGVyYXRvckltcGwnKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQXN5bmNJdGVyYXRvckJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVTdGVhbUFzeW5jSXRlcmF0b3JgKTtcbn1cblxuLy8vIDxyZWZlcmVuY2UgbGliPVwiZXMyMDE1LmNvcmVcIiAvPlxuLy8gaHR0cHM6Ly9kZXZlbG9wZXIubW96aWxsYS5vcmcvZW4tVVMvZG9jcy9XZWIvSmF2YVNjcmlwdC9SZWZlcmVuY2UvR2xvYmFsX09iamVjdHMvTnVtYmVyL2lzTmFOI1BvbHlmaWxsXG5jb25zdCBOdW1iZXJJc05hTiA9IE51bWJlci5pc05hTiB8fCBmdW5jdGlvbiAoeCkge1xuICAgIC8vIGVzbGludC1kaXNhYmxlLW5leHQtbGluZSBuby1zZWxmLWNvbXBhcmVcbiAgICByZXR1cm4geCAhPT0geDtcbn07XG5cbmZ1bmN0aW9uIElzRmluaXRlTm9uTmVnYXRpdmVOdW1iZXIodikge1xuICAgIGlmICghSXNOb25OZWdhdGl2ZU51bWJlcih2KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICh2ID09PSBJbmZpbml0eSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNOb25OZWdhdGl2ZU51bWJlcih2KSB7XG4gICAgaWYgKHR5cGVvZiB2ICE9PSAnbnVtYmVyJykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChOdW1iZXJJc05hTih2KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICh2IDwgMCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuXG5mdW5jdGlvbiBEZXF1ZXVlVmFsdWUoY29udGFpbmVyKSB7XG4gICAgY29uc3QgcGFpciA9IGNvbnRhaW5lci5fcXVldWUuc2hpZnQoKTtcbiAgICBjb250YWluZXIuX3F1ZXVlVG90YWxTaXplIC09IHBhaXIuc2l6ZTtcbiAgICBpZiAoY29udGFpbmVyLl9xdWV1ZVRvdGFsU2l6ZSA8IDApIHtcbiAgICAgICAgY29udGFpbmVyLl9xdWV1ZVRvdGFsU2l6ZSA9IDA7XG4gICAgfVxuICAgIHJldHVybiBwYWlyLnZhbHVlO1xufVxuZnVuY3Rpb24gRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udGFpbmVyLCB2YWx1ZSwgc2l6ZSkge1xuICAgIHNpemUgPSBOdW1iZXIoc2l6ZSk7XG4gICAgaWYgKCFJc0Zpbml0ZU5vbk5lZ2F0aXZlTnVtYmVyKHNpemUpKSB7XG4gICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdTaXplIG11c3QgYmUgYSBmaW5pdGUsIG5vbi1OYU4sIG5vbi1uZWdhdGl2ZSBudW1iZXIuJyk7XG4gICAgfVxuICAgIGNvbnRhaW5lci5fcXVldWUucHVzaCh7IHZhbHVlLCBzaXplIH0pO1xuICAgIGNvbnRhaW5lci5fcXVldWVUb3RhbFNpemUgKz0gc2l6ZTtcbn1cbmZ1bmN0aW9uIFBlZWtRdWV1ZVZhbHVlKGNvbnRhaW5lcikge1xuICAgIGNvbnN0IHBhaXIgPSBjb250YWluZXIuX3F1ZXVlLnBlZWsoKTtcbiAgICByZXR1cm4gcGFpci52YWx1ZTtcbn1cbmZ1bmN0aW9uIFJlc2V0UXVldWUoY29udGFpbmVyKSB7XG4gICAgY29udGFpbmVyLl9xdWV1ZSA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIGNvbnRhaW5lci5fcXVldWVUb3RhbFNpemUgPSAwO1xufVxuXG5mdW5jdGlvbiBDcmVhdGVBcnJheUZyb21MaXN0KGVsZW1lbnRzKSB7XG4gICAgLy8gV2UgdXNlIGFycmF5cyB0byByZXByZXNlbnQgbGlzdHMsIHNvIHRoaXMgaXMgYmFzaWNhbGx5IGEgbm8tb3AuXG4gICAgLy8gRG8gYSBzbGljZSB0aG91Z2gganVzdCBpbiBjYXNlIHdlIGhhcHBlbiB0byBkZXBlbmQgb24gdGhlIHVuaXF1ZS1uZXNzLlxuICAgIHJldHVybiBlbGVtZW50cy5zbGljZSgpO1xufVxuZnVuY3Rpb24gQ29weURhdGFCbG9ja0J5dGVzKGRlc3QsIGRlc3RPZmZzZXQsIHNyYywgc3JjT2Zmc2V0LCBuKSB7XG4gICAgbmV3IFVpbnQ4QXJyYXkoZGVzdCkuc2V0KG5ldyBVaW50OEFycmF5KHNyYywgc3JjT2Zmc2V0LCBuKSwgZGVzdE9mZnNldCk7XG59XG4vLyBOb3QgaW1wbGVtZW50ZWQgY29ycmVjdGx5XG5mdW5jdGlvbiBUcmFuc2ZlckFycmF5QnVmZmVyKE8pIHtcbiAgICByZXR1cm4gTztcbn1cbi8vIE5vdCBpbXBsZW1lbnRlZCBjb3JyZWN0bHlcbmZ1bmN0aW9uIElzRGV0YWNoZWRCdWZmZXIoTykge1xuICAgIHJldHVybiBmYWxzZTtcbn1cblxuLyoqXG4gKiBBIHB1bGwtaW50byByZXF1ZXN0IGluIGEge0BsaW5rIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJ9LlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdCB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgdmlldyBmb3Igd3JpdGluZyBpbiB0bywgb3IgYG51bGxgIGlmIHRoZSBCWU9CIHJlcXVlc3QgaGFzIGFscmVhZHkgYmVlbiByZXNwb25kZWQgdG8uXG4gICAgICovXG4gICAgZ2V0IHZpZXcoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieW9iUmVxdWVzdEJyYW5kQ2hlY2tFeGNlcHRpb24oJ3ZpZXcnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdGhpcy5fdmlldztcbiAgICB9XG4gICAgcmVzcG9uZChieXRlc1dyaXR0ZW4pIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbigncmVzcG9uZCcpO1xuICAgICAgICB9XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoYnl0ZXNXcml0dGVuLCAxLCAncmVzcG9uZCcpO1xuICAgICAgICBieXRlc1dyaXR0ZW4gPSBjb252ZXJ0VW5zaWduZWRMb25nTG9uZ1dpdGhFbmZvcmNlUmFuZ2UoYnl0ZXNXcml0dGVuLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIGlmICh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIEJZT0IgcmVxdWVzdCBoYXMgYmVlbiBpbnZhbGlkYXRlZCcpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc0RldGFjaGVkQnVmZmVyKHRoaXMuX3ZpZXcuYnVmZmVyKSkgO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZCh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciwgYnl0ZXNXcml0dGVuKTtcbiAgICB9XG4gICAgcmVzcG9uZFdpdGhOZXdWaWV3KHZpZXcpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbigncmVzcG9uZFdpdGhOZXdWaWV3Jyk7XG4gICAgICAgIH1cbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudCh2aWV3LCAxLCAncmVzcG9uZFdpdGhOZXdWaWV3Jyk7XG4gICAgICAgIGlmICghQXJyYXlCdWZmZXIuaXNWaWV3KHZpZXcpKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdZb3UgY2FuIG9ubHkgcmVzcG9uZCB3aXRoIGFycmF5IGJ1ZmZlciB2aWV3cycpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh2aWV3LmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2NodW5rIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHZpZXcuYnVmZmVyLmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYGNodW5rJ3MgYnVmZmVyIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoYCk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX2Fzc29jaWF0ZWRSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1RoaXMgQllPQiByZXF1ZXN0IGhhcyBiZWVuIGludmFsaWRhdGVkJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRXaXRoTmV3Vmlldyh0aGlzLl9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciwgdmlldyk7XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdC5wcm90b3R5cGUsIHtcbiAgICByZXNwb25kOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICByZXNwb25kV2l0aE5ld1ZpZXc6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHZpZXc6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1JlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QnLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgUmVhZGFibGVTdHJlYW0gfCByZWFkYWJsZSBieXRlIHN0cmVhbX0ncyBzdGF0ZSBhbmQgaW50ZXJuYWwgcXVldWUuXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyIHtcbiAgICBjb25zdHJ1Y3RvcigpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignSWxsZWdhbCBjb25zdHJ1Y3RvcicpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBjdXJyZW50IEJZT0IgcHVsbCByZXF1ZXN0LCBvciBgbnVsbGAgaWYgdGhlcmUgaXNuJ3Qgb25lLlxuICAgICAqL1xuICAgIGdldCBieW9iUmVxdWVzdCgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5dGVTdHJlYW1Db250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbignYnlvYlJlcXVlc3QnKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fYnlvYlJlcXVlc3QgPT09IG51bGwgJiYgdGhpcy5fcGVuZGluZ1B1bGxJbnRvcy5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICBjb25zdCBmaXJzdERlc2NyaXB0b3IgPSB0aGlzLl9wZW5kaW5nUHVsbEludG9zLnBlZWsoKTtcbiAgICAgICAgICAgIGNvbnN0IHZpZXcgPSBuZXcgVWludDhBcnJheShmaXJzdERlc2NyaXB0b3IuYnVmZmVyLCBmaXJzdERlc2NyaXB0b3IuYnl0ZU9mZnNldCArIGZpcnN0RGVzY3JpcHRvci5ieXRlc0ZpbGxlZCwgZmlyc3REZXNjcmlwdG9yLmJ5dGVMZW5ndGggLSBmaXJzdERlc2NyaXB0b3IuYnl0ZXNGaWxsZWQpO1xuICAgICAgICAgICAgY29uc3QgYnlvYlJlcXVlc3QgPSBPYmplY3QuY3JlYXRlKFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlKTtcbiAgICAgICAgICAgIFNldFVwUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdChieW9iUmVxdWVzdCwgdGhpcywgdmlldyk7XG4gICAgICAgICAgICB0aGlzLl9ieW9iUmVxdWVzdCA9IGJ5b2JSZXF1ZXN0O1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9ieW9iUmVxdWVzdDtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIGNvbnRyb2xsZWQgc3RyZWFtJ3MgaW50ZXJuYWwgcXVldWUuIEl0IGNhbiBiZSBuZWdhdGl2ZSwgaWYgdGhlIHF1ZXVlIGlzXG4gICAgICogb3Zlci1mdWxsLiBBbiB1bmRlcmx5aW5nIGJ5dGUgc291cmNlIG91Z2h0IHRvIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIGRldGVybWluZSB3aGVuIGFuZCBob3cgdG8gYXBwbHkgYmFja3ByZXNzdXJlLlxuICAgICAqL1xuICAgIGdldCBkZXNpcmVkU2l6ZSgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGJ5dGVTdHJlYW1Db250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbignZGVzaXJlZFNpemUnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckdldERlc2lyZWRTaXplKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBDbG9zZXMgdGhlIGNvbnRyb2xsZWQgcmVhZGFibGUgc3RyZWFtLiBDb25zdW1lcnMgd2lsbCBzdGlsbCBiZSBhYmxlIHRvIHJlYWQgYW55IHByZXZpb3VzbHktZW5xdWV1ZWQgY2h1bmtzIGZyb21cbiAgICAgKiB0aGUgc3RyZWFtLCBidXQgb25jZSB0aG9zZSBhcmUgcmVhZCwgdGhlIHN0cmVhbSB3aWxsIGJlY29tZSBjbG9zZWQuXG4gICAgICovXG4gICAgY2xvc2UoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlU3RyZWFtQ29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Nsb3NlJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX2Nsb3NlUmVxdWVzdGVkKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgc3RyZWFtIGhhcyBhbHJlYWR5IGJlZW4gY2xvc2VkOyBkbyBub3QgY2xvc2UgaXQgYWdhaW4hJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RhdGUgPSB0aGlzLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICAgICAgaWYgKHN0YXRlICE9PSAncmVhZGFibGUnKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBUaGUgc3RyZWFtIChpbiAke3N0YXRlfSBzdGF0ZSkgaXMgbm90IGluIHRoZSByZWFkYWJsZSBzdGF0ZSBhbmQgY2Fubm90IGJlIGNsb3NlZGApO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuaykge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudChjaHVuaywgMSwgJ2VucXVldWUnKTtcbiAgICAgICAgaWYgKCFBcnJheUJ1ZmZlci5pc1ZpZXcoY2h1bmspKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdjaHVuayBtdXN0IGJlIGFuIGFycmF5IGJ1ZmZlciB2aWV3Jyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGNodW5rLmJ5dGVMZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2NodW5rIG11c3QgaGF2ZSBub24temVybyBieXRlTGVuZ3RoJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKGNodW5rLmJ1ZmZlci5ieXRlTGVuZ3RoID09PSAwKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGBjaHVuaydzIGJ1ZmZlciBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aGApO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9jbG9zZVJlcXVlc3RlZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignc3RyZWFtIGlzIGNsb3NlZCBvciBkcmFpbmluZycpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHN0YXRlID0gdGhpcy5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbS5fc3RhdGU7XG4gICAgICAgIGlmIChzdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgVGhlIHN0cmVhbSAoaW4gJHtzdGF0ZX0gc3RhdGUpIGlzIG5vdCBpbiB0aGUgcmVhZGFibGUgc3RhdGUgYW5kIGNhbm5vdCBiZSBlbnF1ZXVlZCB0b2ApO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlKHRoaXMsIGNodW5rKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogRXJyb3JzIHRoZSBjb250cm9sbGVkIHJlYWRhYmxlIHN0cmVhbSwgbWFraW5nIGFsbCBmdXR1cmUgaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuXG4gICAgICovXG4gICAgZXJyb3IoZSA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlcnJvcicpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcih0aGlzLCBlKTtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtDYW5jZWxTdGVwc10ocmVhc29uKSB7XG4gICAgICAgIGlmICh0aGlzLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IGZpcnN0RGVzY3JpcHRvciA9IHRoaXMuX3BlbmRpbmdQdWxsSW50b3MucGVlaygpO1xuICAgICAgICAgICAgZmlyc3REZXNjcmlwdG9yLmJ5dGVzRmlsbGVkID0gMDtcbiAgICAgICAgfVxuICAgICAgICBSZXNldFF1ZXVlKHRoaXMpO1xuICAgICAgICBjb25zdCByZXN1bHQgPSB0aGlzLl9jYW5jZWxBbGdvcml0aG0ocmVhc29uKTtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyh0aGlzKTtcbiAgICAgICAgcmV0dXJuIHJlc3VsdDtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtQdWxsU3RlcHNdKHJlYWRSZXF1ZXN0KSB7XG4gICAgICAgIGNvbnN0IHN0cmVhbSA9IHRoaXMuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgICAgIGlmICh0aGlzLl9xdWV1ZVRvdGFsU2l6ZSA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IGVudHJ5ID0gdGhpcy5fcXVldWUuc2hpZnQoKTtcbiAgICAgICAgICAgIHRoaXMuX3F1ZXVlVG90YWxTaXplIC09IGVudHJ5LmJ5dGVMZW5ndGg7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbih0aGlzKTtcbiAgICAgICAgICAgIGNvbnN0IHZpZXcgPSBuZXcgVWludDhBcnJheShlbnRyeS5idWZmZXIsIGVudHJ5LmJ5dGVPZmZzZXQsIGVudHJ5LmJ5dGVMZW5ndGgpO1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2NodW5rU3RlcHModmlldyk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgYXV0b0FsbG9jYXRlQ2h1bmtTaXplID0gdGhpcy5fYXV0b0FsbG9jYXRlQ2h1bmtTaXplO1xuICAgICAgICBpZiAoYXV0b0FsbG9jYXRlQ2h1bmtTaXplICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIGxldCBidWZmZXI7XG4gICAgICAgICAgICB0cnkge1xuICAgICAgICAgICAgICAgIGJ1ZmZlciA9IG5ldyBBcnJheUJ1ZmZlcihhdXRvQWxsb2NhdGVDaHVua1NpemUpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgY2F0Y2ggKGJ1ZmZlckUpIHtcbiAgICAgICAgICAgICAgICByZWFkUmVxdWVzdC5fZXJyb3JTdGVwcyhidWZmZXJFKTtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBjb25zdCBwdWxsSW50b0Rlc2NyaXB0b3IgPSB7XG4gICAgICAgICAgICAgICAgYnVmZmVyLFxuICAgICAgICAgICAgICAgIGJ5dGVPZmZzZXQ6IDAsXG4gICAgICAgICAgICAgICAgYnl0ZUxlbmd0aDogYXV0b0FsbG9jYXRlQ2h1bmtTaXplLFxuICAgICAgICAgICAgICAgIGJ5dGVzRmlsbGVkOiAwLFxuICAgICAgICAgICAgICAgIGVsZW1lbnRTaXplOiAxLFxuICAgICAgICAgICAgICAgIHZpZXdDb25zdHJ1Y3RvcjogVWludDhBcnJheSxcbiAgICAgICAgICAgICAgICByZWFkZXJUeXBlOiAnZGVmYXVsdCdcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgICB0aGlzLl9wZW5kaW5nUHVsbEludG9zLnB1c2gocHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbUFkZFJlYWRSZXF1ZXN0KHN0cmVhbSwgcmVhZFJlcXVlc3QpO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZCh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLnByb3RvdHlwZSwge1xuICAgIGNsb3NlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlbnF1ZXVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlcnJvcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgYnlvYlJlcXVlc3Q6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGRlc2lyZWRTaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5cbmZ1bmN0aW9uIElzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcih4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0nKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHNob3VsZFB1bGwgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hvdWxkQ2FsbFB1bGwoY29udHJvbGxlcik7XG4gICAgaWYgKCFzaG91bGRQdWxsKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3B1bGxpbmcpIHtcbiAgICAgICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gdHJ1ZTtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb250cm9sbGVyLl9wdWxsaW5nID0gdHJ1ZTtcbiAgICAvLyBUT0RPOiBUZXN0IGNvbnRyb2xsZXIgYXJndW1lbnRcbiAgICBjb25zdCBwdWxsUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0oKTtcbiAgICB1cG9uUHJvbWlzZShwdWxsUHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBjb250cm9sbGVyLl9wdWxsaW5nID0gZmFsc2U7XG4gICAgICAgIGlmIChjb250cm9sbGVyLl9wdWxsQWdhaW4pIHtcbiAgICAgICAgICAgIGNvbnRyb2xsZXIuX3B1bGxBZ2FpbiA9IGZhbHNlO1xuICAgICAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgICAgIH1cbiAgICB9LCBlID0+IHtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVycm9yKGNvbnRyb2xsZXIsIGUpO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyUGVuZGluZ1B1bGxJbnRvcyhjb250cm9sbGVyKSB7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckludmFsaWRhdGVCWU9CUmVxdWVzdChjb250cm9sbGVyKTtcbiAgICBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKHN0cmVhbSwgcHVsbEludG9EZXNjcmlwdG9yKSB7XG4gICAgbGV0IGRvbmUgPSBmYWxzZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgZG9uZSA9IHRydWU7XG4gICAgfVxuICAgIGNvbnN0IGZpbGxlZFZpZXcgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29udmVydFB1bGxJbnRvRGVzY3JpcHRvcihwdWxsSW50b0Rlc2NyaXB0b3IpO1xuICAgIGlmIChwdWxsSW50b0Rlc2NyaXB0b3IucmVhZGVyVHlwZSA9PT0gJ2RlZmF1bHQnKSB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgZmlsbGVkVmlldywgZG9uZSk7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbUZ1bGZpbGxSZWFkSW50b1JlcXVlc3Qoc3RyZWFtLCBmaWxsZWRWaWV3LCBkb25lKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29udmVydFB1bGxJbnRvRGVzY3JpcHRvcihwdWxsSW50b0Rlc2NyaXB0b3IpIHtcbiAgICBjb25zdCBieXRlc0ZpbGxlZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZDtcbiAgICBjb25zdCBlbGVtZW50U2l6ZSA9IHB1bGxJbnRvRGVzY3JpcHRvci5lbGVtZW50U2l6ZTtcbiAgICByZXR1cm4gbmV3IHB1bGxJbnRvRGVzY3JpcHRvci52aWV3Q29uc3RydWN0b3IocHVsbEludG9EZXNjcmlwdG9yLmJ1ZmZlciwgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVPZmZzZXQsIGJ5dGVzRmlsbGVkIC8gZWxlbWVudFNpemUpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgYnVmZmVyLCBieXRlT2Zmc2V0LCBieXRlTGVuZ3RoKSB7XG4gICAgY29udHJvbGxlci5fcXVldWUucHVzaCh7IGJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCB9KTtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSArPSBieXRlTGVuZ3RoO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxQdWxsSW50b0Rlc2NyaXB0b3JGcm9tUXVldWUoY29udHJvbGxlciwgcHVsbEludG9EZXNjcmlwdG9yKSB7XG4gICAgY29uc3QgZWxlbWVudFNpemUgPSBwdWxsSW50b0Rlc2NyaXB0b3IuZWxlbWVudFNpemU7XG4gICAgY29uc3QgY3VycmVudEFsaWduZWRCeXRlcyA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCAtIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCAlIGVsZW1lbnRTaXplO1xuICAgIGNvbnN0IG1heEJ5dGVzVG9Db3B5ID0gTWF0aC5taW4oY29udHJvbGxlci5fcXVldWVUb3RhbFNpemUsIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlTGVuZ3RoIC0gcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkKTtcbiAgICBjb25zdCBtYXhCeXRlc0ZpbGxlZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlc0ZpbGxlZCArIG1heEJ5dGVzVG9Db3B5O1xuICAgIGNvbnN0IG1heEFsaWduZWRCeXRlcyA9IG1heEJ5dGVzRmlsbGVkIC0gbWF4Qnl0ZXNGaWxsZWQgJSBlbGVtZW50U2l6ZTtcbiAgICBsZXQgdG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyA9IG1heEJ5dGVzVG9Db3B5O1xuICAgIGxldCByZWFkeSA9IGZhbHNlO1xuICAgIGlmIChtYXhBbGlnbmVkQnl0ZXMgPiBjdXJyZW50QWxpZ25lZEJ5dGVzKSB7XG4gICAgICAgIHRvdGFsQnl0ZXNUb0NvcHlSZW1haW5pbmcgPSBtYXhBbGlnbmVkQnl0ZXMgLSBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQ7XG4gICAgICAgIHJlYWR5ID0gdHJ1ZTtcbiAgICB9XG4gICAgY29uc3QgcXVldWUgPSBjb250cm9sbGVyLl9xdWV1ZTtcbiAgICB3aGlsZSAodG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyA+IDApIHtcbiAgICAgICAgY29uc3QgaGVhZE9mUXVldWUgPSBxdWV1ZS5wZWVrKCk7XG4gICAgICAgIGNvbnN0IGJ5dGVzVG9Db3B5ID0gTWF0aC5taW4odG90YWxCeXRlc1RvQ29weVJlbWFpbmluZywgaGVhZE9mUXVldWUuYnl0ZUxlbmd0aCk7XG4gICAgICAgIGNvbnN0IGRlc3RTdGFydCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkO1xuICAgICAgICBDb3B5RGF0YUJsb2NrQnl0ZXMocHVsbEludG9EZXNjcmlwdG9yLmJ1ZmZlciwgZGVzdFN0YXJ0LCBoZWFkT2ZRdWV1ZS5idWZmZXIsIGhlYWRPZlF1ZXVlLmJ5dGVPZmZzZXQsIGJ5dGVzVG9Db3B5KTtcbiAgICAgICAgaWYgKGhlYWRPZlF1ZXVlLmJ5dGVMZW5ndGggPT09IGJ5dGVzVG9Db3B5KSB7XG4gICAgICAgICAgICBxdWV1ZS5zaGlmdCgpO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgaGVhZE9mUXVldWUuYnl0ZU9mZnNldCArPSBieXRlc1RvQ29weTtcbiAgICAgICAgICAgIGhlYWRPZlF1ZXVlLmJ5dGVMZW5ndGggLT0gYnl0ZXNUb0NvcHk7XG4gICAgICAgIH1cbiAgICAgICAgY29udHJvbGxlci5fcXVldWVUb3RhbFNpemUgLT0gYnl0ZXNUb0NvcHk7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGaWxsSGVhZFB1bGxJbnRvRGVzY3JpcHRvcihjb250cm9sbGVyLCBieXRlc1RvQ29weSwgcHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgdG90YWxCeXRlc1RvQ29weVJlbWFpbmluZyAtPSBieXRlc1RvQ29weTtcbiAgICB9XG4gICAgcmV0dXJuIHJlYWR5O1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxIZWFkUHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIsIHNpemUsIHB1bGxJbnRvRGVzY3JpcHRvcikge1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJJbnZhbGlkYXRlQllPQlJlcXVlc3QoY29udHJvbGxlcik7XG4gICAgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICs9IHNpemU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbihjb250cm9sbGVyKSB7XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID09PSAwICYmIGNvbnRyb2xsZXIuX2Nsb3NlUmVxdWVzdGVkKSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtQ2xvc2UoY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbSk7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySW52YWxpZGF0ZUJZT0JSZXF1ZXN0KGNvbnRyb2xsZXIpIHtcbiAgICBpZiAoY29udHJvbGxlci5fYnlvYlJlcXVlc3QgPT09IG51bGwpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb250cm9sbGVyLl9ieW9iUmVxdWVzdC5fYXNzb2NpYXRlZFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIgPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fYnlvYlJlcXVlc3QuX3ZpZXcgPSBudWxsO1xuICAgIGNvbnRyb2xsZXIuX2J5b2JSZXF1ZXN0ID0gbnVsbDtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJQcm9jZXNzUHVsbEludG9EZXNjcmlwdG9yc1VzaW5nUXVldWUoY29udHJvbGxlcikge1xuICAgIHdoaWxlIChjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID09PSAwKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcHVsbEludG9EZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgICAgIGlmIChSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRmlsbFB1bGxJbnRvRGVzY3JpcHRvckZyb21RdWV1ZShjb250cm9sbGVyLCBwdWxsSW50b0Rlc2NyaXB0b3IpKSB7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8oY29udHJvbGxlcik7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0sIHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgICAgIH1cbiAgICB9XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUHVsbEludG8oY29udHJvbGxlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBsZXQgZWxlbWVudFNpemUgPSAxO1xuICAgIGlmICh2aWV3LmNvbnN0cnVjdG9yICE9PSBEYXRhVmlldykge1xuICAgICAgICBlbGVtZW50U2l6ZSA9IHZpZXcuY29uc3RydWN0b3IuQllURVNfUEVSX0VMRU1FTlQ7XG4gICAgfVxuICAgIGNvbnN0IGN0b3IgPSB2aWV3LmNvbnN0cnVjdG9yO1xuICAgIGNvbnN0IGJ1ZmZlciA9IFRyYW5zZmVyQXJyYXlCdWZmZXIodmlldy5idWZmZXIpO1xuICAgIGNvbnN0IHB1bGxJbnRvRGVzY3JpcHRvciA9IHtcbiAgICAgICAgYnVmZmVyLFxuICAgICAgICBieXRlT2Zmc2V0OiB2aWV3LmJ5dGVPZmZzZXQsXG4gICAgICAgIGJ5dGVMZW5ndGg6IHZpZXcuYnl0ZUxlbmd0aCxcbiAgICAgICAgYnl0ZXNGaWxsZWQ6IDAsXG4gICAgICAgIGVsZW1lbnRTaXplLFxuICAgICAgICB2aWV3Q29uc3RydWN0b3I6IGN0b3IsXG4gICAgICAgIHJlYWRlclR5cGU6ICdieW9iJ1xuICAgIH07XG4gICAgaWYgKGNvbnRyb2xsZXIuX3BlbmRpbmdQdWxsSW50b3MubGVuZ3RoID4gMCkge1xuICAgICAgICBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnB1c2gocHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICAgICAgLy8gTm8gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoKSBjYWxsIHNpbmNlOlxuICAgICAgICAvLyAtIE5vIGNoYW5nZSBoYXBwZW5zIG9uIGRlc2lyZWRTaXplXG4gICAgICAgIC8vIC0gVGhlIHNvdXJjZSBoYXMgYWxyZWFkeSBiZWVuIG5vdGlmaWVkIG9mIHRoYXQgdGhlcmUncyBhdCBsZWFzdCAxIHBlbmRpbmcgcmVhZCh2aWV3KVxuICAgICAgICBSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIHJlYWRJbnRvUmVxdWVzdCk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKHN0cmVhbS5fc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIGNvbnN0IGVtcHR5VmlldyA9IG5ldyBjdG9yKHB1bGxJbnRvRGVzY3JpcHRvci5idWZmZXIsIHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0LCAwKTtcbiAgICAgICAgcmVhZEludG9SZXF1ZXN0Ll9jbG9zZVN0ZXBzKGVtcHR5Vmlldyk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID4gMCkge1xuICAgICAgICBpZiAoUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxQdWxsSW50b0Rlc2NyaXB0b3JGcm9tUXVldWUoY29udHJvbGxlciwgcHVsbEludG9EZXNjcmlwdG9yKSkge1xuICAgICAgICAgICAgY29uc3QgZmlsbGVkVmlldyA9IFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb252ZXJ0UHVsbEludG9EZXNjcmlwdG9yKHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVySGFuZGxlUXVldWVEcmFpbihjb250cm9sbGVyKTtcbiAgICAgICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fY2h1bmtTdGVwcyhmaWxsZWRWaWV3KTtcbiAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgfVxuICAgICAgICBpZiAoY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQpIHtcbiAgICAgICAgICAgIGNvbnN0IGUgPSBuZXcgVHlwZUVycm9yKCdJbnN1ZmZpY2llbnQgYnl0ZXMgdG8gZmlsbCBlbGVtZW50cyBpbiB0aGUgZ2l2ZW4gYnVmZmVyJyk7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSk7XG4gICAgICAgICAgICByZWFkSW50b1JlcXVlc3QuX2Vycm9yU3RlcHMoZSk7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICB9XG4gICAgY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wdXNoKHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgUmVhZGFibGVTdHJlYW1BZGRSZWFkSW50b1JlcXVlc3Qoc3RyZWFtLCByZWFkSW50b1JlcXVlc3QpO1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJbkNsb3NlZFN0YXRlKGNvbnRyb2xsZXIsIGZpcnN0RGVzY3JpcHRvcikge1xuICAgIGZpcnN0RGVzY3JpcHRvci5idWZmZXIgPSBUcmFuc2ZlckFycmF5QnVmZmVyKGZpcnN0RGVzY3JpcHRvci5idWZmZXIpO1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIHdoaWxlIChSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRJbnRvUmVxdWVzdHMoc3RyZWFtKSA+IDApIHtcbiAgICAgICAgICAgIGNvbnN0IHB1bGxJbnRvRGVzY3JpcHRvciA9IFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJTaGlmdFBlbmRpbmdQdWxsSW50byhjb250cm9sbGVyKTtcbiAgICAgICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDb21taXRQdWxsSW50b0Rlc2NyaXB0b3Ioc3RyZWFtLCBwdWxsSW50b0Rlc2NyaXB0b3IpO1xuICAgICAgICB9XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJblJlYWRhYmxlU3RhdGUoY29udHJvbGxlciwgYnl0ZXNXcml0dGVuLCBwdWxsSW50b0Rlc2NyaXB0b3IpIHtcbiAgICBpZiAocHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICsgYnl0ZXNXcml0dGVuID4gcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVMZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ2J5dGVzV3JpdHRlbiBvdXQgb2YgcmFuZ2UnKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckZpbGxIZWFkUHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIsIGJ5dGVzV3JpdHRlbiwgcHVsbEludG9EZXNjcmlwdG9yKTtcbiAgICBpZiAocHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkIDwgcHVsbEludG9EZXNjcmlwdG9yLmVsZW1lbnRTaXplKSB7XG4gICAgICAgIC8vIFRPRE86IEZpZ3VyZSBvdXQgd2hldGhlciB3ZSBzaG91bGQgZGV0YWNoIHRoZSBidWZmZXIgb3Igbm90IGhlcmUuXG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclNoaWZ0UGVuZGluZ1B1bGxJbnRvKGNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IHJlbWFpbmRlclNpemUgPSBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQgJSBwdWxsSW50b0Rlc2NyaXB0b3IuZWxlbWVudFNpemU7XG4gICAgaWYgKHJlbWFpbmRlclNpemUgPiAwKSB7XG4gICAgICAgIGNvbnN0IGVuZCA9IHB1bGxJbnRvRGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgcHVsbEludG9EZXNjcmlwdG9yLmJ5dGVzRmlsbGVkO1xuICAgICAgICBjb25zdCByZW1haW5kZXIgPSBwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyLnNsaWNlKGVuZCAtIHJlbWFpbmRlclNpemUsIGVuZCk7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFbnF1ZXVlQ2h1bmtUb1F1ZXVlKGNvbnRyb2xsZXIsIHJlbWFpbmRlciwgMCwgcmVtYWluZGVyLmJ5dGVMZW5ndGgpO1xuICAgIH1cbiAgICBwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyID0gVHJhbnNmZXJBcnJheUJ1ZmZlcihwdWxsSW50b0Rlc2NyaXB0b3IuYnVmZmVyKTtcbiAgICBwdWxsSW50b0Rlc2NyaXB0b3IuYnl0ZXNGaWxsZWQgLT0gcmVtYWluZGVyU2l6ZTtcbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ29tbWl0UHVsbEludG9EZXNjcmlwdG9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0sIHB1bGxJbnRvRGVzY3JpcHRvcik7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclByb2Nlc3NQdWxsSW50b0Rlc2NyaXB0b3JzVXNpbmdRdWV1ZShjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW50ZXJuYWwoY29udHJvbGxlciwgYnl0ZXNXcml0dGVuKSB7XG4gICAgY29uc3QgZmlyc3REZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIGlmIChieXRlc1dyaXR0ZW4gIT09IDApIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ2J5dGVzV3JpdHRlbiBtdXN0IGJlIDAgd2hlbiBjYWxsaW5nIHJlc3BvbmQoKSBvbiBhIGNsb3NlZCBzdHJlYW0nKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZEluQ2xvc2VkU3RhdGUoY29udHJvbGxlciwgZmlyc3REZXNjcmlwdG9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW5SZWFkYWJsZVN0YXRlKGNvbnRyb2xsZXIsIGJ5dGVzV3JpdHRlbiwgZmlyc3REZXNjcmlwdG9yKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyU2hpZnRQZW5kaW5nUHVsbEludG8oY29udHJvbGxlcikge1xuICAgIGNvbnN0IGRlc2NyaXB0b3IgPSBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnNoaWZ0KCk7XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckludmFsaWRhdGVCWU9CUmVxdWVzdChjb250cm9sbGVyKTtcbiAgICByZXR1cm4gZGVzY3JpcHRvcjtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChSZWFkYWJsZVN0cmVhbUhhc0RlZmF1bHRSZWFkZXIoc3RyZWFtKSAmJiBSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pICYmIFJlYWRhYmxlU3RyZWFtR2V0TnVtUmVhZEludG9SZXF1ZXN0cyhzdHJlYW0pID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgY29uc3QgZGVzaXJlZFNpemUgPSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyR2V0RGVzaXJlZFNpemUoY29udHJvbGxlcik7XG4gICAgaWYgKGRlc2lyZWRTaXplID4gMCkge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIGZhbHNlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKSB7XG4gICAgY29udHJvbGxlci5fcHVsbEFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9jYW5jZWxBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG59XG4vLyBBIGNsaWVudCBvZiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyIG1heSB1c2UgdGhlc2UgZnVuY3Rpb25zIGRpcmVjdGx5IHRvIGJ5cGFzcyBzdGF0ZSBjaGVjay5cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbG9zZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgfHwgc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA+IDApIHtcbiAgICAgICAgY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgPSB0cnVlO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLmxlbmd0aCA+IDApIHtcbiAgICAgICAgY29uc3QgZmlyc3RQZW5kaW5nUHVsbEludG8gPSBjb250cm9sbGVyLl9wZW5kaW5nUHVsbEludG9zLnBlZWsoKTtcbiAgICAgICAgaWYgKGZpcnN0UGVuZGluZ1B1bGxJbnRvLmJ5dGVzRmlsbGVkID4gMCkge1xuICAgICAgICAgICAgY29uc3QgZSA9IG5ldyBUeXBlRXJyb3IoJ0luc3VmZmljaWVudCBieXRlcyB0byBmaWxsIGVsZW1lbnRzIGluIHRoZSBnaXZlbiBidWZmZXInKTtcbiAgICAgICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKTtcbiAgICAgICAgICAgIHRocm93IGU7XG4gICAgICAgIH1cbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRW5xdWV1ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW07XG4gICAgaWYgKGNvbnRyb2xsZXIuX2Nsb3NlUmVxdWVzdGVkIHx8IHN0cmVhbS5fc3RhdGUgIT09ICdyZWFkYWJsZScpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBjb25zdCBidWZmZXIgPSBjaHVuay5idWZmZXI7XG4gICAgY29uc3QgYnl0ZU9mZnNldCA9IGNodW5rLmJ5dGVPZmZzZXQ7XG4gICAgY29uc3QgYnl0ZUxlbmd0aCA9IGNodW5rLmJ5dGVMZW5ndGg7XG4gICAgY29uc3QgdHJhbnNmZXJyZWRCdWZmZXIgPSBUcmFuc2ZlckFycmF5QnVmZmVyKGJ1ZmZlcik7XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtSGFzRGVmYXVsdFJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIGlmIChSZWFkYWJsZVN0cmVhbUdldE51bVJlYWRSZXF1ZXN0cyhzdHJlYW0pID09PSAwKSB7XG4gICAgICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRW5xdWV1ZUNodW5rVG9RdWV1ZShjb250cm9sbGVyLCB0cmFuc2ZlcnJlZEJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBjb25zdCB0cmFuc2ZlcnJlZFZpZXcgPSBuZXcgVWludDhBcnJheSh0cmFuc2ZlcnJlZEJ1ZmZlciwgYnl0ZU9mZnNldCwgYnl0ZUxlbmd0aCk7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbUZ1bGZpbGxSZWFkUmVxdWVzdChzdHJlYW0sIHRyYW5zZmVycmVkVmlldywgZmFsc2UpO1xuICAgICAgICB9XG4gICAgfVxuICAgIGVsc2UgaWYgKFJlYWRhYmxlU3RyZWFtSGFzQllPQlJlYWRlcihzdHJlYW0pKSB7XG4gICAgICAgIC8vIFRPRE86IElkZWFsbHkgaW4gdGhpcyBicmFuY2ggZGV0YWNoaW5nIHNob3VsZCBoYXBwZW4gb25seSBpZiB0aGUgYnVmZmVyIGlzIG5vdCBjb25zdW1lZCBmdWxseS5cbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgdHJhbnNmZXJyZWRCdWZmZXIsIGJ5dGVPZmZzZXQsIGJ5dGVMZW5ndGgpO1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUHJvY2Vzc1B1bGxJbnRvRGVzY3JpcHRvcnNVc2luZ1F1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlckVucXVldWVDaHVua1RvUXVldWUoY29udHJvbGxlciwgdHJhbnNmZXJyZWRCdWZmZXIsIGJ5dGVPZmZzZXQsIGJ5dGVMZW5ndGgpO1xuICAgIH1cbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlQnl0ZVN0cmVhbTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSAhPT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDbGVhclBlbmRpbmdQdWxsSW50b3MoY29udHJvbGxlcik7XG4gICAgUmVzZXRRdWV1ZShjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFJlYWRhYmxlU3RyZWFtRXJyb3Ioc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVCeXRlU3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmVkJykge1xuICAgICAgICByZXR1cm4gbnVsbDtcbiAgICB9XG4gICAgaWYgKHN0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICByZXR1cm4gMDtcbiAgICB9XG4gICAgcmV0dXJuIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNIC0gY29udHJvbGxlci5fcXVldWVUb3RhbFNpemU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyUmVzcG9uZChjb250cm9sbGVyLCBieXRlc1dyaXR0ZW4pIHtcbiAgICBieXRlc1dyaXR0ZW4gPSBOdW1iZXIoYnl0ZXNXcml0dGVuKTtcbiAgICBpZiAoIUlzRmluaXRlTm9uTmVnYXRpdmVOdW1iZXIoYnl0ZXNXcml0dGVuKSkge1xuICAgICAgICB0aHJvdyBuZXcgUmFuZ2VFcnJvcignYnl0ZXNXcml0dGVuIG11c3QgYmUgYSBmaW5pdGUnKTtcbiAgICB9XG4gICAgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRJbnRlcm5hbChjb250cm9sbGVyLCBieXRlc1dyaXR0ZW4pO1xufVxuZnVuY3Rpb24gUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlclJlc3BvbmRXaXRoTmV3Vmlldyhjb250cm9sbGVyLCB2aWV3KSB7XG4gICAgY29uc3QgZmlyc3REZXNjcmlwdG9yID0gY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcy5wZWVrKCk7XG4gICAgaWYgKGZpcnN0RGVzY3JpcHRvci5ieXRlT2Zmc2V0ICsgZmlyc3REZXNjcmlwdG9yLmJ5dGVzRmlsbGVkICE9PSB2aWV3LmJ5dGVPZmZzZXQpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSByZWdpb24gc3BlY2lmaWVkIGJ5IHZpZXcgZG9lcyBub3QgbWF0Y2ggYnlvYlJlcXVlc3QnKTtcbiAgICB9XG4gICAgaWYgKGZpcnN0RGVzY3JpcHRvci5ieXRlTGVuZ3RoICE9PSB2aWV3LmJ5dGVMZW5ndGgpIHtcbiAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSBidWZmZXIgb2YgdmlldyBoYXMgZGlmZmVyZW50IGNhcGFjaXR5IHRoYW4gYnlvYlJlcXVlc3QnKTtcbiAgICB9XG4gICAgZmlyc3REZXNjcmlwdG9yLmJ1ZmZlciA9IHZpZXcuYnVmZmVyO1xuICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJSZXNwb25kSW50ZXJuYWwoY29udHJvbGxlciwgdmlldy5ieXRlTGVuZ3RoKTtcbn1cbmZ1bmN0aW9uIFNldFVwUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcihzdHJlYW0sIGNvbnRyb2xsZXIsIHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmssIGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZUJ5dGVTdHJlYW0gPSBzdHJlYW07XG4gICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gZmFsc2U7XG4gICAgY29udHJvbGxlci5fcHVsbGluZyA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX2J5b2JSZXF1ZXN0ID0gbnVsbDtcbiAgICAvLyBOZWVkIHRvIHNldCB0aGUgc2xvdHMgc28gdGhhdCB0aGUgYXNzZXJ0IGRvZXNuJ3QgZmlyZS4gSW4gdGhlIHNwZWMgdGhlIHNsb3RzIGFscmVhZHkgZXhpc3QgaW1wbGljaXRseS5cbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IGNvbnRyb2xsZXIuX3F1ZXVlVG90YWxTaXplID0gdW5kZWZpbmVkO1xuICAgIFJlc2V0UXVldWUoY29udHJvbGxlcik7XG4gICAgY29udHJvbGxlci5fY2xvc2VSZXF1ZXN0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdGFydGVkID0gZmFsc2U7XG4gICAgY29udHJvbGxlci5fc3RyYXRlZ3lIV00gPSBoaWdoV2F0ZXJNYXJrO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0gPSBwdWxsQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX2NhbmNlbEFsZ29yaXRobSA9IGNhbmNlbEFsZ29yaXRobTtcbiAgICBjb250cm9sbGVyLl9hdXRvQWxsb2NhdGVDaHVua1NpemUgPSBhdXRvQWxsb2NhdGVDaHVua1NpemU7XG4gICAgY29udHJvbGxlci5fcGVuZGluZ1B1bGxJbnRvcyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIHN0cmVhbS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICBjb25zdCBzdGFydFJlc3VsdCA9IHN0YXJ0QWxnb3JpdGhtKCk7XG4gICAgdXBvblByb21pc2UocHJvbWlzZVJlc29sdmVkV2l0aChzdGFydFJlc3VsdCksICgpID0+IHtcbiAgICAgICAgY29udHJvbGxlci5fc3RhcnRlZCA9IHRydWU7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKGNvbnRyb2xsZXIpO1xuICAgIH0sIHIgPT4ge1xuICAgICAgICBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgcik7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NvdXJjZShzdHJlYW0sIHVuZGVybHlpbmdCeXRlU291cmNlLCBoaWdoV2F0ZXJNYXJrKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5wcm90b3R5cGUpO1xuICAgIGxldCBzdGFydEFsZ29yaXRobSA9ICgpID0+IHVuZGVmaW5lZDtcbiAgICBsZXQgcHVsbEFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBsZXQgY2FuY2VsQWxnb3JpdGhtID0gKCkgPT4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0YXJ0QWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ0J5dGVTb3VyY2Uuc3RhcnQoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5wdWxsICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcHVsbEFsZ29yaXRobSA9ICgpID0+IHVuZGVybHlpbmdCeXRlU291cmNlLnB1bGwoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nQnl0ZVNvdXJjZS5jYW5jZWwgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBjYW5jZWxBbGdvcml0aG0gPSByZWFzb24gPT4gdW5kZXJseWluZ0J5dGVTb3VyY2UuY2FuY2VsKHJlYXNvbik7XG4gICAgfVxuICAgIGNvbnN0IGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSA9IHVuZGVybHlpbmdCeXRlU291cmNlLmF1dG9BbGxvY2F0ZUNodW5rU2l6ZTtcbiAgICBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsQWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBhdXRvQWxsb2NhdGVDaHVua1NpemUpO1xufVxuZnVuY3Rpb24gU2V0VXBSZWFkYWJsZVN0cmVhbUJZT0JSZXF1ZXN0KHJlcXVlc3QsIGNvbnRyb2xsZXIsIHZpZXcpIHtcbiAgICByZXF1ZXN0Ll9hc3NvY2lhdGVkUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlciA9IGNvbnRyb2xsZXI7XG4gICAgcmVxdWVzdC5fdmlldyA9IHZpZXc7XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1CWU9CUmVxdWVzdC5cbmZ1bmN0aW9uIGJ5b2JSZXF1ZXN0QnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3RgKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBSZWFkYWJsZUJ5dGVTdHJlYW1Db250cm9sbGVyLlxuZnVuY3Rpb24gYnl0ZVN0cmVhbUNvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcmApO1xufVxuXG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW0uXG5mdW5jdGlvbiBBY3F1aXJlUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHN0cmVhbSkge1xuICAgIHJldHVybiBuZXcgUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHN0cmVhbSk7XG59XG4vLyBSZWFkYWJsZVN0cmVhbSBBUEkgZXhwb3NlZCBmb3IgY29udHJvbGxlcnMuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUFkZFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIHJlYWRJbnRvUmVxdWVzdCkge1xuICAgIHN0cmVhbS5fcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLnB1c2gocmVhZEludG9SZXF1ZXN0KTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRJbnRvUmVxdWVzdChzdHJlYW0sIGNodW5rLCBkb25lKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgY29uc3QgcmVhZEludG9SZXF1ZXN0ID0gcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLnNoaWZ0KCk7XG4gICAgaWYgKGRvbmUpIHtcbiAgICAgICAgcmVhZEludG9SZXF1ZXN0Ll9jbG9zZVN0ZXBzKGNodW5rKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fY2h1bmtTdGVwcyhjaHVuayk7XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkSW50b1JlcXVlc3RzKHN0cmVhbSkge1xuICAgIHJldHVybiBzdHJlYW0uX3JlYWRlci5fcmVhZEludG9SZXF1ZXN0cy5sZW5ndGg7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUhhc0JZT0JSZWFkZXIoc3RyZWFtKSB7XG4gICAgY29uc3QgcmVhZGVyID0gc3RyZWFtLl9yZWFkZXI7XG4gICAgaWYgKHJlYWRlciA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtQllPQlJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG4vKipcbiAqIEEgQllPQiByZWFkZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyJyk7XG4gICAgICAgIGFzc2VydFJlYWRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHJlYWRpbmcgYnkgYW5vdGhlciByZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoIUlzUmVhZGFibGVCeXRlU3RyZWFtQ29udHJvbGxlcihzdHJlYW0uX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlcikpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjb25zdHJ1Y3QgYSBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIgZm9yIGEgc3RyZWFtIG5vdCBjb25zdHJ1Y3RlZCB3aXRoIGEgYnl0ZSAnICtcbiAgICAgICAgICAgICAgICAnc291cmNlJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljSW5pdGlhbGl6ZSh0aGlzLCBzdHJlYW0pO1xuICAgICAgICB0aGlzLl9yZWFkSW50b1JlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgYSBwcm9taXNlIHRoYXQgd2lsbCBiZSBmdWxmaWxsZWQgd2hlbiB0aGUgc3RyZWFtIGJlY29tZXMgY2xvc2VkLCBvciByZWplY3RlZCBpZiB0aGUgc3RyZWFtIGV2ZXIgZXJyb3JzIG9yXG4gICAgICogdGhlIHJlYWRlcidzIGxvY2sgaXMgcmVsZWFzZWQgYmVmb3JlIHRoZSBzdHJlYW0gZmluaXNoZXMgY2xvc2luZy5cbiAgICAgKi9cbiAgICBnZXQgY2xvc2VkKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2VkJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jbG9zZWRQcm9taXNlO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBJZiB0aGUgcmVhZGVyIGlzIGFjdGl2ZSwgYmVoYXZlcyB0aGUgc2FtZSBhcyB7QGxpbmsgUmVhZGFibGVTdHJlYW0uY2FuY2VsIHwgc3RyZWFtLmNhbmNlbChyZWFzb24pfS5cbiAgICAgKi9cbiAgICBjYW5jZWwocmVhc29uID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGJ5b2JSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdjYW5jZWwnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyUmVhZGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgocmVhZGVyTG9ja0V4Y2VwdGlvbignY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNDYW5jZWwodGhpcywgcmVhc29uKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQXR0ZW1wdHMgdG8gcmVhZHMgYnl0ZXMgaW50byB2aWV3LCBhbmQgcmV0dXJucyBhIHByb21pc2UgcmVzb2x2ZWQgd2l0aCB0aGUgcmVzdWx0LlxuICAgICAqXG4gICAgICogSWYgcmVhZGluZyBhIGNodW5rIGNhdXNlcyB0aGUgcXVldWUgdG8gYmVjb21lIGVtcHR5LCBtb3JlIGRhdGEgd2lsbCBiZSBwdWxsZWQgZnJvbSB0aGUgdW5kZXJseWluZyBzb3VyY2UuXG4gICAgICovXG4gICAgcmVhZCh2aWV3KSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGJ5b2JSZWFkZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdyZWFkJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghQXJyYXlCdWZmZXIuaXNWaWV3KHZpZXcpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCd2aWV3IG11c3QgYmUgYW4gYXJyYXkgYnVmZmVyIHZpZXcnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHZpZXcuYnl0ZUxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcigndmlldyBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aCcpKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodmlldy5idWZmZXIuYnl0ZUxlbmd0aCA9PT0gMCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcihgdmlldydzIGJ1ZmZlciBtdXN0IGhhdmUgbm9uLXplcm8gYnl0ZUxlbmd0aGApKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChyZWFkZXJMb2NrRXhjZXB0aW9uKCdyZWFkIGZyb20nKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IHJlc29sdmVQcm9taXNlO1xuICAgICAgICBsZXQgcmVqZWN0UHJvbWlzZTtcbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICAgICAgcmVzb2x2ZVByb21pc2UgPSByZXNvbHZlO1xuICAgICAgICAgICAgcmVqZWN0UHJvbWlzZSA9IHJlamVjdDtcbiAgICAgICAgfSk7XG4gICAgICAgIGNvbnN0IHJlYWRJbnRvUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9jaHVua1N0ZXBzOiBjaHVuayA9PiByZXNvbHZlUHJvbWlzZSh7IHZhbHVlOiBjaHVuaywgZG9uZTogZmFsc2UgfSksXG4gICAgICAgICAgICBfY2xvc2VTdGVwczogY2h1bmsgPT4gcmVzb2x2ZVByb21pc2UoeyB2YWx1ZTogY2h1bmssIGRvbmU6IHRydWUgfSksXG4gICAgICAgICAgICBfZXJyb3JTdGVwczogZSA9PiByZWplY3RQcm9taXNlKGUpXG4gICAgICAgIH07XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlclJlYWQodGhpcywgdmlldywgcmVhZEludG9SZXF1ZXN0KTtcbiAgICAgICAgcmV0dXJuIHByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJlbGVhc2VzIHRoZSByZWFkZXIncyBsb2NrIG9uIHRoZSBjb3JyZXNwb25kaW5nIHN0cmVhbS4gQWZ0ZXIgdGhlIGxvY2sgaXMgcmVsZWFzZWQsIHRoZSByZWFkZXIgaXMgbm8gbG9uZ2VyIGFjdGl2ZS5cbiAgICAgKiBJZiB0aGUgYXNzb2NpYXRlZCBzdHJlYW0gaXMgZXJyb3JlZCB3aGVuIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmVhZGVyIHdpbGwgYXBwZWFyIGVycm9yZWQgaW4gdGhlIHNhbWUgd2F5XG4gICAgICogZnJvbSBub3cgb247IG90aGVyd2lzZSwgdGhlIHJlYWRlciB3aWxsIGFwcGVhciBjbG9zZWQuXG4gICAgICpcbiAgICAgKiBBIHJlYWRlcidzIGxvY2sgY2Fubm90IGJlIHJlbGVhc2VkIHdoaWxlIGl0IHN0aWxsIGhhcyBhIHBlbmRpbmcgcmVhZCByZXF1ZXN0LCBpLmUuLCBpZiBhIHByb21pc2UgcmV0dXJuZWQgYnlcbiAgICAgKiB0aGUgcmVhZGVyJ3Mge0BsaW5rIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5yZWFkIHwgcmVhZCgpfSBtZXRob2QgaGFzIG5vdCB5ZXQgYmVlbiBzZXR0bGVkLiBBdHRlbXB0aW5nIHRvXG4gICAgICogZG8gc28gd2lsbCB0aHJvdyBhIGBUeXBlRXJyb3JgIGFuZCBsZWF2ZSB0aGUgcmVhZGVyIGxvY2tlZCB0byB0aGUgc3RyZWFtLlxuICAgICAqL1xuICAgIHJlbGVhc2VMb2NrKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieW9iUmVhZGVyQnJhbmRDaGVja0V4Y2VwdGlvbigncmVsZWFzZUxvY2snKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAodGhpcy5fb3duZXJSZWFkYWJsZVN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX3JlYWRJbnRvUmVxdWVzdHMubGVuZ3RoID4gMCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignVHJpZWQgdG8gcmVsZWFzZSBhIHJlYWRlciBsb2NrIHdoZW4gdGhhdCByZWFkZXIgaGFzIHBlbmRpbmcgcmVhZCgpIGNhbGxzIHVuLXNldHRsZWQnKTtcbiAgICAgICAgfVxuICAgICAgICBSZWFkYWJsZVN0cmVhbVJlYWRlckdlbmVyaWNSZWxlYXNlKHRoaXMpO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5wcm90b3R5cGUsIHtcbiAgICBjYW5jZWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlYWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlbGVhc2VMb2NrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBjbG9zZWQ6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlci5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgcmVhZGVycy5cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfcmVhZEludG9SZXF1ZXN0cycpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXJSZWFkKHJlYWRlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KSB7XG4gICAgY29uc3Qgc3RyZWFtID0gcmVhZGVyLl9vd25lclJlYWRhYmxlU3RyZWFtO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gdHJ1ZTtcbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fZXJyb3JTdGVwcyhzdHJlYW0uX3N0b3JlZEVycm9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJQdWxsSW50byhzdHJlYW0uX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgdmlldywgcmVhZEludG9SZXF1ZXN0KTtcbiAgICB9XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyLlxuZnVuY3Rpb24gYnlvYlJlYWRlckJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtQllPQlJlYWRlcmApO1xufVxuXG5mdW5jdGlvbiBFeHRyYWN0SGlnaFdhdGVyTWFyayhzdHJhdGVneSwgZGVmYXVsdEhXTSkge1xuICAgIGNvbnN0IHsgaGlnaFdhdGVyTWFyayB9ID0gc3RyYXRlZ3k7XG4gICAgaWYgKGhpZ2hXYXRlck1hcmsgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gZGVmYXVsdEhXTTtcbiAgICB9XG4gICAgaWYgKE51bWJlcklzTmFOKGhpZ2hXYXRlck1hcmspIHx8IGhpZ2hXYXRlck1hcmsgPCAwKSB7XG4gICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdJbnZhbGlkIGhpZ2hXYXRlck1hcmsnKTtcbiAgICB9XG4gICAgcmV0dXJuIGhpZ2hXYXRlck1hcms7XG59XG5mdW5jdGlvbiBFeHRyYWN0U2l6ZUFsZ29yaXRobShzdHJhdGVneSkge1xuICAgIGNvbnN0IHsgc2l6ZSB9ID0gc3RyYXRlZ3k7XG4gICAgaWYgKCFzaXplKSB7XG4gICAgICAgIHJldHVybiAoKSA9PiAxO1xuICAgIH1cbiAgICByZXR1cm4gc2l6ZTtcbn1cblxuZnVuY3Rpb24gY29udmVydFF1ZXVpbmdTdHJhdGVneShpbml0LCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShpbml0LCBjb250ZXh0KTtcbiAgICBjb25zdCBoaWdoV2F0ZXJNYXJrID0gaW5pdCA9PT0gbnVsbCB8fCBpbml0ID09PSB2b2lkIDAgPyB2b2lkIDAgOiBpbml0LmhpZ2hXYXRlck1hcms7XG4gICAgY29uc3Qgc2l6ZSA9IGluaXQgPT09IG51bGwgfHwgaW5pdCA9PT0gdm9pZCAwID8gdm9pZCAwIDogaW5pdC5zaXplO1xuICAgIHJldHVybiB7XG4gICAgICAgIGhpZ2hXYXRlck1hcms6IGhpZ2hXYXRlck1hcmsgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRVbnJlc3RyaWN0ZWREb3VibGUoaGlnaFdhdGVyTWFyayksXG4gICAgICAgIHNpemU6IHNpemUgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lTaXplKHNpemUsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3NpemUnIHRoYXRgKVxuICAgIH07XG59XG5mdW5jdGlvbiBjb252ZXJ0UXVldWluZ1N0cmF0ZWd5U2l6ZShmbiwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gY2h1bmsgPT4gY29udmVydFVucmVzdHJpY3RlZERvdWJsZShmbihjaHVuaykpO1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0VW5kZXJseWluZ1Npbmsob3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KG9yaWdpbmFsLCBjb250ZXh0KTtcbiAgICBjb25zdCBhYm9ydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5hYm9ydDtcbiAgICBjb25zdCBjbG9zZSA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5jbG9zZTtcbiAgICBjb25zdCBzdGFydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5zdGFydDtcbiAgICBjb25zdCB0eXBlID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnR5cGU7XG4gICAgY29uc3Qgd3JpdGUgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwud3JpdGU7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgYWJvcnQ6IGFib3J0ID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Fib3J0Q2FsbGJhY2soYWJvcnQsIG9yaWdpbmFsLCBgJHtjb250ZXh0fSBoYXMgbWVtYmVyICdhYm9ydCcgdGhhdGApLFxuICAgICAgICBjbG9zZTogY2xvc2UgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuZGVybHlpbmdTaW5rQ2xvc2VDYWxsYmFjayhjbG9zZSwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ2Nsb3NlJyB0aGF0YCksXG4gICAgICAgIHN0YXJ0OiBzdGFydCA9PT0gdW5kZWZpbmVkID9cbiAgICAgICAgICAgIHVuZGVmaW5lZCA6XG4gICAgICAgICAgICBjb252ZXJ0VW5kZXJseWluZ1NpbmtTdGFydENhbGxiYWNrKHN0YXJ0LCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnc3RhcnQnIHRoYXRgKSxcbiAgICAgICAgd3JpdGU6IHdyaXRlID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU2lua1dyaXRlQ2FsbGJhY2sod3JpdGUsIG9yaWdpbmFsLCBgJHtjb250ZXh0fSBoYXMgbWVtYmVyICd3cml0ZScgdGhhdGApLFxuICAgICAgICB0eXBlXG4gICAgfTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Fib3J0Q2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAocmVhc29uKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtyZWFzb25dKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua0Nsb3NlQ2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAoKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU2lua1N0YXJ0Q2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAoY29udHJvbGxlcikgPT4gcmVmbGVjdENhbGwoZm4sIG9yaWdpbmFsLCBbY29udHJvbGxlcl0pO1xufVxuZnVuY3Rpb24gY29udmVydFVuZGVybHlpbmdTaW5rV3JpdGVDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjaHVuaywgY29udHJvbGxlcikgPT4gcHJvbWlzZUNhbGwoZm4sIG9yaWdpbmFsLCBbY2h1bmssIGNvbnRyb2xsZXJdKTtcbn1cblxuZnVuY3Rpb24gYXNzZXJ0V3JpdGFibGVTdHJlYW0oeCwgY29udGV4dCkge1xuICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh4KSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhIFdyaXRhYmxlU3RyZWFtLmApO1xuICAgIH1cbn1cblxuLyoqXG4gKiBBIHdyaXRhYmxlIHN0cmVhbSByZXByZXNlbnRzIGEgZGVzdGluYXRpb24gZm9yIGRhdGEsIGludG8gd2hpY2ggeW91IGNhbiB3cml0ZS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFdyaXRhYmxlU3RyZWFtIHtcbiAgICBjb25zdHJ1Y3RvcihyYXdVbmRlcmx5aW5nU2luayA9IHt9LCByYXdTdHJhdGVneSA9IHt9KSB7XG4gICAgICAgIGlmIChyYXdVbmRlcmx5aW5nU2luayA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByYXdVbmRlcmx5aW5nU2luayA9IG51bGw7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBhc3NlcnRPYmplY3QocmF3VW5kZXJseWluZ1NpbmssICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBzdHJhdGVneSA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3kocmF3U3RyYXRlZ3ksICdTZWNvbmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIGNvbnN0IHVuZGVybHlpbmdTaW5rID0gY29udmVydFVuZGVybHlpbmdTaW5rKHJhd1VuZGVybHlpbmdTaW5rLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbSh0aGlzKTtcbiAgICAgICAgY29uc3QgdHlwZSA9IHVuZGVybHlpbmdTaW5rLnR5cGU7XG4gICAgICAgIGlmICh0eXBlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBSYW5nZUVycm9yKCdJbnZhbGlkIHR5cGUgaXMgc3BlY2lmaWVkJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc2l6ZUFsZ29yaXRobSA9IEV4dHJhY3RTaXplQWxnb3JpdGhtKHN0cmF0ZWd5KTtcbiAgICAgICAgY29uc3QgaGlnaFdhdGVyTWFyayA9IEV4dHJhY3RIaWdoV2F0ZXJNYXJrKHN0cmF0ZWd5LCAxKTtcbiAgICAgICAgU2V0VXBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTaW5rKHRoaXMsIHVuZGVybHlpbmdTaW5rLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB3aGV0aGVyIG9yIG5vdCB0aGUgd3JpdGFibGUgc3RyZWFtIGlzIGxvY2tlZCB0byBhIHdyaXRlci5cbiAgICAgKi9cbiAgICBnZXQgbG9ja2VkKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24oJ2xvY2tlZCcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBJc1dyaXRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBBYm9ydHMgdGhlIHN0cmVhbSwgc2lnbmFsaW5nIHRoYXQgdGhlIHByb2R1Y2VyIGNhbiBubyBsb25nZXIgc3VjY2Vzc2Z1bGx5IHdyaXRlIHRvIHRoZSBzdHJlYW0gYW5kIGl0IGlzIHRvIGJlXG4gICAgICogaW1tZWRpYXRlbHkgbW92ZWQgdG8gYW4gZXJyb3JlZCBzdGF0ZSwgd2l0aCBhbnkgcXVldWVkLXVwIHdyaXRlcyBkaXNjYXJkZWQuIFRoaXMgd2lsbCBhbHNvIGV4ZWN1dGUgYW55IGFib3J0XG4gICAgICogbWVjaGFuaXNtIG9mIHRoZSB1bmRlcmx5aW5nIHNpbmsuXG4gICAgICpcbiAgICAgKiBUaGUgcmV0dXJuZWQgcHJvbWlzZSB3aWxsIGZ1bGZpbGwgaWYgdGhlIHN0cmVhbSBzaHV0cyBkb3duIHN1Y2Nlc3NmdWxseSwgb3IgcmVqZWN0IGlmIHRoZSB1bmRlcmx5aW5nIHNpbmsgc2lnbmFsZWRcbiAgICAgKiB0aGF0IHRoZXJlIHdhcyBhbiBlcnJvciBkb2luZyBzby4gQWRkaXRpb25hbGx5LCBpdCB3aWxsIHJlamVjdCB3aXRoIGEgYFR5cGVFcnJvcmAgKHdpdGhvdXQgYXR0ZW1wdGluZyB0byBjYW5jZWxcbiAgICAgKiB0aGUgc3RyZWFtKSBpZiB0aGUgc3RyZWFtIGlzIGN1cnJlbnRseSBsb2NrZWQuXG4gICAgICovXG4gICAgYWJvcnQocmVhc29uID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbignYWJvcnQnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBhYm9ydCBhIHN0cmVhbSB0aGF0IGFscmVhZHkgaGFzIGEgd3JpdGVyJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBXcml0YWJsZVN0cmVhbUFib3J0KHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENsb3NlcyB0aGUgc3RyZWFtLiBUaGUgdW5kZXJseWluZyBzaW5rIHdpbGwgZmluaXNoIHByb2Nlc3NpbmcgYW55IHByZXZpb3VzbHktd3JpdHRlbiBjaHVua3MsIGJlZm9yZSBpbnZva2luZyBpdHNcbiAgICAgKiBjbG9zZSBiZWhhdmlvci4gRHVyaW5nIHRoaXMgdGltZSBhbnkgZnVydGhlciBhdHRlbXB0cyB0byB3cml0ZSB3aWxsIGZhaWwgKHdpdGhvdXQgZXJyb3JpbmcgdGhlIHN0cmVhbSkuXG4gICAgICpcbiAgICAgKiBUaGUgbWV0aG9kIHJldHVybnMgYSBwcm9taXNlIHRoYXQgd2lsbCBmdWxmaWxsIGlmIGFsbCByZW1haW5pbmcgY2h1bmtzIGFyZSBzdWNjZXNzZnVsbHkgd3JpdHRlbiBhbmQgdGhlIHN0cmVhbVxuICAgICAqIHN1Y2Nlc3NmdWxseSBjbG9zZXMsIG9yIHJlamVjdHMgaWYgYW4gZXJyb3IgaXMgZW5jb3VudGVyZWQgZHVyaW5nIHRoaXMgcHJvY2Vzcy4gQWRkaXRpb25hbGx5LCBpdCB3aWxsIHJlamVjdCB3aXRoXG4gICAgICogYSBgVHlwZUVycm9yYCAod2l0aG91dCBhdHRlbXB0aW5nIHRvIGNhbmNlbCB0aGUgc3RyZWFtKSBpZiB0aGUgc3RyZWFtIGlzIGN1cnJlbnRseSBsb2NrZWQuXG4gICAgICovXG4gICAgY2xvc2UoKSB7XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjbG9zZSBhIHN0cmVhbSB0aGF0IGFscmVhZHkgaGFzIGEgd3JpdGVyJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodCh0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcignQ2Fubm90IGNsb3NlIGFuIGFscmVhZHktY2xvc2luZyBzdHJlYW0nKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtQ2xvc2UodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENyZWF0ZXMgYSB7QGxpbmsgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyIHwgd3JpdGVyfSBhbmQgbG9ja3MgdGhlIHN0cmVhbSB0byB0aGUgbmV3IHdyaXRlci4gV2hpbGUgdGhlIHN0cmVhbVxuICAgICAqIGlzIGxvY2tlZCwgbm8gb3RoZXIgd3JpdGVyIGNhbiBiZSBhY3F1aXJlZCB1bnRpbCB0aGlzIG9uZSBpcyByZWxlYXNlZC5cbiAgICAgKlxuICAgICAqIFRoaXMgZnVuY3Rpb25hbGl0eSBpcyBlc3BlY2lhbGx5IHVzZWZ1bCBmb3IgY3JlYXRpbmcgYWJzdHJhY3Rpb25zIHRoYXQgZGVzaXJlIHRoZSBhYmlsaXR5IHRvIHdyaXRlIHRvIGEgc3RyZWFtXG4gICAgICogd2l0aG91dCBpbnRlcnJ1cHRpb24gb3IgaW50ZXJsZWF2aW5nLiBCeSBnZXR0aW5nIGEgd3JpdGVyIGZvciB0aGUgc3RyZWFtLCB5b3UgY2FuIGVuc3VyZSBub2JvZHkgZWxzZSBjYW4gd3JpdGUgYXRcbiAgICAgKiB0aGUgc2FtZSB0aW1lLCB3aGljaCB3b3VsZCBjYXVzZSB0aGUgcmVzdWx0aW5nIHdyaXR0ZW4gZGF0YSB0byBiZSB1bnByZWRpY3RhYmxlIGFuZCBwcm9iYWJseSB1c2VsZXNzLlxuICAgICAqL1xuICAgIGdldFdyaXRlcigpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uKCdnZXRXcml0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gQWNxdWlyZVdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICBhYm9ydDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2U6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGdldFdyaXRlcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgbG9ja2VkOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnV3JpdGFibGVTdHJlYW0nLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEFic3RyYWN0IG9wZXJhdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbS5cbmZ1bmN0aW9uIEFjcXVpcmVXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoc3RyZWFtKSB7XG4gICAgcmV0dXJuIG5ldyBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoc3RyZWFtKTtcbn1cbi8vIFRocm93cyBpZiBhbmQgb25seSBpZiBzdGFydEFsZ29yaXRobSB0aHJvd3MuXG5mdW5jdGlvbiBDcmVhdGVXcml0YWJsZVN0cmVhbShzdGFydEFsZ29yaXRobSwgd3JpdGVBbGdvcml0aG0sIGNsb3NlQWxnb3JpdGhtLCBhYm9ydEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyayA9IDEsIHNpemVBbGdvcml0aG0gPSAoKSA9PiAxKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gT2JqZWN0LmNyZWF0ZShXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUpO1xuICAgIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbShzdHJlYW0pO1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgd3JpdGVBbGdvcml0aG0sIGNsb3NlQWxnb3JpdGhtLCBhYm9ydEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgcmV0dXJuIHN0cmVhbTtcbn1cbmZ1bmN0aW9uIEluaXRpYWxpemVXcml0YWJsZVN0cmVhbShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ3dyaXRhYmxlJztcbiAgICAvLyBUaGUgZXJyb3IgdGhhdCB3aWxsIGJlIHJlcG9ydGVkIGJ5IG5ldyBtZXRob2QgY2FsbHMgb25jZSB0aGUgc3RhdGUgYmVjb21lcyBlcnJvcmVkLiBPbmx5IHNldCB3aGVuIFtbc3RhdGVdXSBpc1xuICAgIC8vICdlcnJvcmluZycgb3IgJ2Vycm9yZWQnLiBNYXkgYmUgc2V0IHRvIGFuIHVuZGVmaW5lZCB2YWx1ZS5cbiAgICBzdHJlYW0uX3N0b3JlZEVycm9yID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fd3JpdGVyID0gdW5kZWZpbmVkO1xuICAgIC8vIEluaXRpYWxpemUgdG8gdW5kZWZpbmVkIGZpcnN0IGJlY2F1c2UgdGhlIGNvbnN0cnVjdG9yIG9mIHRoZSBjb250cm9sbGVyIGNoZWNrcyB0aGlzXG4gICAgLy8gdmFyaWFibGUgdG8gdmFsaWRhdGUgdGhlIGNhbGxlci5cbiAgICBzdHJlYW0uX3dyaXRhYmxlU3RyZWFtQ29udHJvbGxlciA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGlzIHF1ZXVlIGlzIHBsYWNlZCBoZXJlIGluc3RlYWQgb2YgdGhlIHdyaXRlciBjbGFzcyBpbiBvcmRlciB0byBhbGxvdyBmb3IgcGFzc2luZyBhIHdyaXRlciB0byB0aGUgbmV4dCBkYXRhXG4gICAgLy8gcHJvZHVjZXIgd2l0aG91dCB3YWl0aW5nIGZvciB0aGUgcXVldWVkIHdyaXRlcyB0byBmaW5pc2guXG4gICAgc3RyZWFtLl93cml0ZVJlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgLy8gV3JpdGUgcmVxdWVzdHMgYXJlIHJlbW92ZWQgZnJvbSBfd3JpdGVSZXF1ZXN0cyB3aGVuIHdyaXRlKCkgaXMgY2FsbGVkIG9uIHRoZSB1bmRlcmx5aW5nIHNpbmsuIFRoaXMgcHJldmVudHNcbiAgICAvLyB0aGVtIGZyb20gYmVpbmcgZXJyb25lb3VzbHkgcmVqZWN0ZWQgb24gZXJyb3IuIElmIGEgd3JpdGUoKSBjYWxsIGlzIGluLWZsaWdodCwgdGhlIHJlcXVlc3QgaXMgc3RvcmVkIGhlcmUuXG4gICAgc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGUgcHJvbWlzZSB0aGF0IHdhcyByZXR1cm5lZCBmcm9tIHdyaXRlci5jbG9zZSgpLiBTdG9yZWQgaGVyZSBiZWNhdXNlIGl0IG1heSBiZSBmdWxmaWxsZWQgYWZ0ZXIgdGhlIHdyaXRlclxuICAgIC8vIGhhcyBiZWVuIGRldGFjaGVkLlxuICAgIHN0cmVhbS5fY2xvc2VSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIC8vIENsb3NlIHJlcXVlc3QgaXMgcmVtb3ZlZCBmcm9tIF9jbG9zZVJlcXVlc3Qgd2hlbiBjbG9zZSgpIGlzIGNhbGxlZCBvbiB0aGUgdW5kZXJseWluZyBzaW5rLiBUaGlzIHByZXZlbnRzIGl0XG4gICAgLy8gZnJvbSBiZWluZyBlcnJvbmVvdXNseSByZWplY3RlZCBvbiBlcnJvci4gSWYgYSBjbG9zZSgpIGNhbGwgaXMgaW4tZmxpZ2h0LCB0aGUgcmVxdWVzdCBpcyBzdG9yZWQgaGVyZS5cbiAgICBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIC8vIFRoZSBwcm9taXNlIHRoYXQgd2FzIHJldHVybmVkIGZyb20gd3JpdGVyLmFib3J0KCkuIFRoaXMgbWF5IGFsc28gYmUgZnVsZmlsbGVkIGFmdGVyIHRoZSB3cml0ZXIgaGFzIGRldGFjaGVkLlxuICAgIHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBUaGUgYmFja3ByZXNzdXJlIHNpZ25hbCBzZXQgYnkgdGhlIGNvbnRyb2xsZXIuXG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmUgPSBmYWxzZTtcbn1cbmZ1bmN0aW9uIElzV3JpdGFibGVTdHJlYW0oeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ193cml0YWJsZVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl93cml0ZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1BYm9ydChzdHJlYW0sIHJlYXNvbikge1xuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnIHx8IHN0YXRlID09PSAnZXJyb3JlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgaWYgKHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybiBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QuX3Byb21pc2U7XG4gICAgfVxuICAgIGxldCB3YXNBbHJlYWR5RXJyb3JpbmcgPSBmYWxzZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgd2FzQWxyZWFkeUVycm9yaW5nID0gdHJ1ZTtcbiAgICAgICAgLy8gcmVhc29uIHdpbGwgbm90IGJlIHVzZWQsIHNvIGRvbid0IGtlZXAgYSByZWZlcmVuY2UgdG8gaXQuXG4gICAgICAgIHJlYXNvbiA9IHVuZGVmaW5lZDtcbiAgICB9XG4gICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB7XG4gICAgICAgICAgICBfcHJvbWlzZTogdW5kZWZpbmVkLFxuICAgICAgICAgICAgX3Jlc29sdmU6IHJlc29sdmUsXG4gICAgICAgICAgICBfcmVqZWN0OiByZWplY3QsXG4gICAgICAgICAgICBfcmVhc29uOiByZWFzb24sXG4gICAgICAgICAgICBfd2FzQWxyZWFkeUVycm9yaW5nOiB3YXNBbHJlYWR5RXJyb3JpbmdcbiAgICAgICAgfTtcbiAgICB9KTtcbiAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QuX3Byb21pc2UgPSBwcm9taXNlO1xuICAgIGlmICghd2FzQWxyZWFkeUVycm9yaW5nKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtU3RhcnRFcnJvcmluZyhzdHJlYW0sIHJlYXNvbik7XG4gICAgfVxuICAgIHJldHVybiBwcm9taXNlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1DbG9zZShzdHJlYW0pIHtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnY2xvc2VkJyB8fCBzdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoYFRoZSBzdHJlYW0gKGluICR7c3RhdGV9IHN0YXRlKSBpcyBub3QgaW4gdGhlIHdyaXRhYmxlIHN0YXRlIGFuZCBjYW5ub3QgYmUgY2xvc2VkYCkpO1xuICAgIH1cbiAgICBjb25zdCBwcm9taXNlID0gbmV3UHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGNvbnN0IGNsb3NlUmVxdWVzdCA9IHtcbiAgICAgICAgICAgIF9yZXNvbHZlOiByZXNvbHZlLFxuICAgICAgICAgICAgX3JlamVjdDogcmVqZWN0XG4gICAgICAgIH07XG4gICAgICAgIHN0cmVhbS5fY2xvc2VSZXF1ZXN0ID0gY2xvc2VSZXF1ZXN0O1xuICAgIH0pO1xuICAgIGNvbnN0IHdyaXRlciA9IHN0cmVhbS5fd3JpdGVyO1xuICAgIGlmICh3cml0ZXIgIT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2JhY2twcmVzc3VyZSAmJiBzdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzb2x2ZSh3cml0ZXIpO1xuICAgIH1cbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xvc2Uoc3RyZWFtLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgIHJldHVybiBwcm9taXNlO1xufVxuLy8gV3JpdGFibGVTdHJlYW0gQVBJIGV4cG9zZWQgZm9yIGNvbnRyb2xsZXJzLlxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1BZGRXcml0ZVJlcXVlc3Qoc3RyZWFtKSB7XG4gICAgY29uc3QgcHJvbWlzZSA9IG5ld1Byb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgICAgICBjb25zdCB3cml0ZVJlcXVlc3QgPSB7XG4gICAgICAgICAgICBfcmVzb2x2ZTogcmVzb2x2ZSxcbiAgICAgICAgICAgIF9yZWplY3Q6IHJlamVjdFxuICAgICAgICB9O1xuICAgICAgICBzdHJlYW0uX3dyaXRlUmVxdWVzdHMucHVzaCh3cml0ZVJlcXVlc3QpO1xuICAgIH0pO1xuICAgIHJldHVybiBwcm9taXNlO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbihzdHJlYW0sIGVycm9yKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBzdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICBXcml0YWJsZVN0cmVhbVN0YXJ0RXJyb3Jpbmcoc3RyZWFtLCBlcnJvcik7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgV3JpdGFibGVTdHJlYW1GaW5pc2hFcnJvcmluZyhzdHJlYW0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1TdGFydEVycm9yaW5nKHN0cmVhbSwgcmVhc29uKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIHN0cmVhbS5fc3RhdGUgPSAnZXJyb3JpbmcnO1xuICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSByZWFzb247XG4gICAgY29uc3Qgd3JpdGVyID0gc3RyZWFtLl93cml0ZXI7XG4gICAgaWYgKHdyaXRlciAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZVJlYWR5UHJvbWlzZVJlamVjdGVkKHdyaXRlciwgcmVhc29uKTtcbiAgICB9XG4gICAgaWYgKCFXcml0YWJsZVN0cmVhbUhhc09wZXJhdGlvbk1hcmtlZEluRmxpZ2h0KHN0cmVhbSkgJiYgY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEVycm9yaW5nKHN0cmVhbSk7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hFcnJvcmluZyhzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ2Vycm9yZWQnO1xuICAgIHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyW0Vycm9yU3RlcHNdKCk7XG4gICAgY29uc3Qgc3RvcmVkRXJyb3IgPSBzdHJlYW0uX3N0b3JlZEVycm9yO1xuICAgIHN0cmVhbS5fd3JpdGVSZXF1ZXN0cy5mb3JFYWNoKHdyaXRlUmVxdWVzdCA9PiB7XG4gICAgICAgIHdyaXRlUmVxdWVzdC5fcmVqZWN0KHN0b3JlZEVycm9yKTtcbiAgICB9KTtcbiAgICBzdHJlYW0uX3dyaXRlUmVxdWVzdHMgPSBuZXcgU2ltcGxlUXVldWUoKTtcbiAgICBpZiAoc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IGFib3J0UmVxdWVzdCA9IHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdDtcbiAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgaWYgKGFib3J0UmVxdWVzdC5fd2FzQWxyZWFkeUVycm9yaW5nKSB7XG4gICAgICAgIGFib3J0UmVxdWVzdC5fcmVqZWN0KHN0b3JlZEVycm9yKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHByb21pc2UgPSBzdHJlYW0uX3dyaXRhYmxlU3RyZWFtQ29udHJvbGxlcltBYm9ydFN0ZXBzXShhYm9ydFJlcXVlc3QuX3JlYXNvbik7XG4gICAgdXBvblByb21pc2UocHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBhYm9ydFJlcXVlc3QuX3Jlc29sdmUoKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgIH0sIChyZWFzb24pID0+IHtcbiAgICAgICAgYWJvcnRSZXF1ZXN0Ll9yZWplY3QocmVhc29uKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlKHN0cmVhbSkge1xuICAgIHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QuX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICBzdHJlYW0uX2luRmxpZ2h0V3JpdGVSZXF1ZXN0ID0gdW5kZWZpbmVkO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1GaW5pc2hJbkZsaWdodFdyaXRlV2l0aEVycm9yKHN0cmVhbSwgZXJyb3IpIHtcbiAgICBzdHJlYW0uX2luRmxpZ2h0V3JpdGVSZXF1ZXN0Ll9yZWplY3QoZXJyb3IpO1xuICAgIHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgV3JpdGFibGVTdHJlYW1EZWFsV2l0aFJlamVjdGlvbihzdHJlYW0sIGVycm9yKTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRDbG9zZShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0Ll9yZXNvbHZlKHVuZGVmaW5lZCk7XG4gICAgc3RyZWFtLl9pbkZsaWdodENsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JpbmcnKSB7XG4gICAgICAgIC8vIFRoZSBlcnJvciB3YXMgdG9vIGxhdGUgdG8gZG8gYW55dGhpbmcsIHNvIGl0IGlzIGlnbm9yZWQuXG4gICAgICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0Ll9yZXNvbHZlKCk7XG4gICAgICAgICAgICBzdHJlYW0uX3BlbmRpbmdBYm9ydFJlcXVlc3QgPSB1bmRlZmluZWQ7XG4gICAgICAgIH1cbiAgICB9XG4gICAgc3RyZWFtLl9zdGF0ZSA9ICdjbG9zZWQnO1xuICAgIGNvbnN0IHdyaXRlciA9IHN0cmVhbS5fd3JpdGVyO1xuICAgIGlmICh3cml0ZXIgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlc29sdmUod3JpdGVyKTtcbiAgICB9XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2VXaXRoRXJyb3Ioc3RyZWFtLCBlcnJvcikge1xuICAgIHN0cmVhbS5faW5GbGlnaHRDbG9zZVJlcXVlc3QuX3JlamVjdChlcnJvcik7XG4gICAgc3RyZWFtLl9pbkZsaWdodENsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICAvLyBOZXZlciBleGVjdXRlIHNpbmsgYWJvcnQoKSBhZnRlciBzaW5rIGNsb3NlKCkuXG4gICAgaWYgKHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0cmVhbS5fcGVuZGluZ0Fib3J0UmVxdWVzdC5fcmVqZWN0KGVycm9yKTtcbiAgICAgICAgc3RyZWFtLl9wZW5kaW5nQWJvcnRSZXF1ZXN0ID0gdW5kZWZpbmVkO1xuICAgIH1cbiAgICBXcml0YWJsZVN0cmVhbURlYWxXaXRoUmVqZWN0aW9uKHN0cmVhbSwgZXJyb3IpO1xufVxuLy8gVE9ETyhyaWNlYSk6IEZpeCBhbHBoYWJldGljYWwgb3JkZXIuXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl9jbG9zZVJlcXVlc3QgPT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtSGFzT3BlcmF0aW9uTWFya2VkSW5GbGlnaHQoc3RyZWFtKSB7XG4gICAgaWYgKHN0cmVhbS5faW5GbGlnaHRXcml0ZVJlcXVlc3QgPT09IHVuZGVmaW5lZCAmJiBzdHJlYW0uX2luRmxpZ2h0Q2xvc2VSZXF1ZXN0ID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSkge1xuICAgIHN0cmVhbS5faW5GbGlnaHRDbG9zZVJlcXVlc3QgPSBzdHJlYW0uX2Nsb3NlUmVxdWVzdDtcbiAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtTWFya0ZpcnN0V3JpdGVSZXF1ZXN0SW5GbGlnaHQoc3RyZWFtKSB7XG4gICAgc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCA9IHN0cmVhbS5fd3JpdGVSZXF1ZXN0cy5zaGlmdCgpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1SZWplY3RDbG9zZUFuZENsb3NlZFByb21pc2VJZk5lZWRlZChzdHJlYW0pIHtcbiAgICBpZiAoc3RyZWFtLl9jbG9zZVJlcXVlc3QgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdC5fcmVqZWN0KHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICBzdHJlYW0uX2Nsb3NlUmVxdWVzdCA9IHVuZGVmaW5lZDtcbiAgICB9XG4gICAgY29uc3Qgd3JpdGVyID0gc3RyZWFtLl93cml0ZXI7XG4gICAgaWYgKHdyaXRlciAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVqZWN0KHdyaXRlciwgc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1VcGRhdGVCYWNrcHJlc3N1cmUoc3RyZWFtLCBiYWNrcHJlc3N1cmUpIHtcbiAgICBjb25zdCB3cml0ZXIgPSBzdHJlYW0uX3dyaXRlcjtcbiAgICBpZiAod3JpdGVyICE9PSB1bmRlZmluZWQgJiYgYmFja3ByZXNzdXJlICE9PSBzdHJlYW0uX2JhY2twcmVzc3VyZSkge1xuICAgICAgICBpZiAoYmFja3ByZXNzdXJlKSB7XG4gICAgICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQod3JpdGVyKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNvbHZlKHdyaXRlcik7XG4gICAgICAgIH1cbiAgICB9XG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmUgPSBiYWNrcHJlc3N1cmU7XG59XG4vKipcbiAqIEEgZGVmYXVsdCB3cml0ZXIgdmVuZGVkIGJ5IGEge0BsaW5rIFdyaXRhYmxlU3RyZWFtfS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciB7XG4gICAgY29uc3RydWN0b3Ioc3RyZWFtKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQoc3RyZWFtLCAxLCAnV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyJyk7XG4gICAgICAgIGFzc2VydFdyaXRhYmxlU3RyZWFtKHN0cmVhbSwgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNXcml0YWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGlzIHN0cmVhbSBoYXMgYWxyZWFkeSBiZWVuIGxvY2tlZCBmb3IgZXhjbHVzaXZlIHdyaXRpbmcgYnkgYW5vdGhlciB3cml0ZXInKTtcbiAgICAgICAgfVxuICAgICAgICB0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgICAgICBzdHJlYW0uX3dyaXRlciA9IHRoaXM7XG4gICAgICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICAgICAgaWYgKHN0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgICAgICBpZiAoIVdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkgJiYgc3RyZWFtLl9iYWNrcHJlc3N1cmUpIHtcbiAgICAgICAgICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZSh0aGlzKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh0aGlzKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh0aGlzKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIGlmIChzdGF0ZSA9PT0gJ2Vycm9yaW5nJykge1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHRoaXMsIHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplKHRoaXMpO1xuICAgICAgICB9XG4gICAgICAgIGVsc2UgaWYgKHN0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHRoaXMpO1xuICAgICAgICAgICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh0aGlzKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGNvbnN0IHN0b3JlZEVycm9yID0gc3RyZWFtLl9zdG9yZWRFcnJvcjtcbiAgICAgICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCh0aGlzLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHRoaXMsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgfVxuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIHN0cmVhbSBiZWNvbWVzIGNsb3NlZCwgb3IgcmVqZWN0ZWQgaWYgdGhlIHN0cmVhbSBldmVyIGVycm9ycyBvclxuICAgICAqIHRoZSB3cml0ZXLigJlzIGxvY2sgaXMgcmVsZWFzZWQgYmVmb3JlIHRoZSBzdHJlYW0gZmluaXNoZXMgY2xvc2luZy5cbiAgICAgKi9cbiAgICBnZXQgY2xvc2VkKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2VkJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jbG9zZWRQcm9taXNlO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBkZXNpcmVkIHNpemUgdG8gZmlsbCB0aGUgc3RyZWFt4oCZcyBpbnRlcm5hbCBxdWV1ZS4gSXQgY2FuIGJlIG5lZ2F0aXZlLCBpZiB0aGUgcXVldWUgaXMgb3Zlci1mdWxsLlxuICAgICAqIEEgcHJvZHVjZXIgY2FuIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIGRldGVybWluZSB0aGUgcmlnaHQgYW1vdW50IG9mIGRhdGEgdG8gd3JpdGUuXG4gICAgICpcbiAgICAgKiBJdCB3aWxsIGJlIGBudWxsYCBpZiB0aGUgc3RyZWFtIGNhbm5vdCBiZSBzdWNjZXNzZnVsbHkgd3JpdHRlbiB0byAoZHVlIHRvIGVpdGhlciBiZWluZyBlcnJvcmVkLCBvciBoYXZpbmcgYW4gYWJvcnRcbiAgICAgKiBxdWV1ZWQgdXApLiBJdCB3aWxsIHJldHVybiB6ZXJvIGlmIHRoZSBzdHJlYW0gaXMgY2xvc2VkLiBBbmQgdGhlIGdldHRlciB3aWxsIHRocm93IGFuIGV4Y2VwdGlvbiBpZiBpbnZva2VkIHdoZW5cbiAgICAgKiB0aGUgd3JpdGVy4oCZcyBsb2NrIGlzIHJlbGVhc2VkLlxuICAgICAqL1xuICAgIGdldCBkZXNpcmVkU2l6ZSgpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKHRoaXMuX293bmVyV3JpdGFibGVTdHJlYW0gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckxvY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckdldERlc2lyZWRTaXplKHRoaXMpO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIGEgcHJvbWlzZSB0aGF0IHdpbGwgYmUgZnVsZmlsbGVkIHdoZW4gdGhlIGRlc2lyZWQgc2l6ZSB0byBmaWxsIHRoZSBzdHJlYW3igJlzIGludGVybmFsIHF1ZXVlIHRyYW5zaXRpb25zXG4gICAgICogZnJvbSBub24tcG9zaXRpdmUgdG8gcG9zaXRpdmUsIHNpZ25hbGluZyB0aGF0IGl0IGlzIG5vIGxvbmdlciBhcHBseWluZyBiYWNrcHJlc3N1cmUuIE9uY2UgdGhlIGRlc2lyZWQgc2l6ZSBkaXBzXG4gICAgICogYmFjayB0byB6ZXJvIG9yIGJlbG93LCB0aGUgZ2V0dGVyIHdpbGwgcmV0dXJuIGEgbmV3IHByb21pc2UgdGhhdCBzdGF5cyBwZW5kaW5nIHVudGlsIHRoZSBuZXh0IHRyYW5zaXRpb24uXG4gICAgICpcbiAgICAgKiBJZiB0aGUgc3RyZWFtIGJlY29tZXMgZXJyb3JlZCBvciBhYm9ydGVkLCBvciB0aGUgd3JpdGVy4oCZcyBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgcmV0dXJuZWQgcHJvbWlzZSB3aWxsIGJlY29tZVxuICAgICAqIHJlamVjdGVkLlxuICAgICAqL1xuICAgIGdldCByZWFkeSgpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlYWR5JykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9yZWFkeVByb21pc2U7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIElmIHRoZSByZWFkZXIgaXMgYWN0aXZlLCBiZWhhdmVzIHRoZSBzYW1lIGFzIHtAbGluayBXcml0YWJsZVN0cmVhbS5hYm9ydCB8IHN0cmVhbS5hYm9ydChyZWFzb24pfS5cbiAgICAgKi9cbiAgICBhYm9ydChyZWFzb24gPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Fib3J0JykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGRlZmF1bHRXcml0ZXJMb2NrRXhjZXB0aW9uKCdhYm9ydCcpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyQWJvcnQodGhpcywgcmVhc29uKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogSWYgdGhlIHJlYWRlciBpcyBhY3RpdmUsIGJlaGF2ZXMgdGhlIHNhbWUgYXMge0BsaW5rIFdyaXRhYmxlU3RyZWFtLmNsb3NlIHwgc3RyZWFtLmNsb3NlKCl9LlxuICAgICAqL1xuICAgIGNsb3NlKCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyZWFtID0gdGhpcy5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICAgICAgaWYgKHN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbignY2xvc2UnKSk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKFdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCBjbG9zZSBhbiBhbHJlYWR5LWNsb3Npbmcgc3RyZWFtJykpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmVsZWFzZXMgdGhlIHdyaXRlcuKAmXMgbG9jayBvbiB0aGUgY29ycmVzcG9uZGluZyBzdHJlYW0uIEFmdGVyIHRoZSBsb2NrIGlzIHJlbGVhc2VkLCB0aGUgd3JpdGVyIGlzIG5vIGxvbmdlciBhY3RpdmUuXG4gICAgICogSWYgdGhlIGFzc29jaWF0ZWQgc3RyZWFtIGlzIGVycm9yZWQgd2hlbiB0aGUgbG9jayBpcyByZWxlYXNlZCwgdGhlIHdyaXRlciB3aWxsIGFwcGVhciBlcnJvcmVkIGluIHRoZSBzYW1lIHdheSBmcm9tXG4gICAgICogbm93IG9uOyBvdGhlcndpc2UsIHRoZSB3cml0ZXIgd2lsbCBhcHBlYXIgY2xvc2VkLlxuICAgICAqXG4gICAgICogTm90ZSB0aGF0IHRoZSBsb2NrIGNhbiBzdGlsbCBiZSByZWxlYXNlZCBldmVuIGlmIHNvbWUgb25nb2luZyB3cml0ZXMgaGF2ZSBub3QgeWV0IGZpbmlzaGVkIChpLmUuIGV2ZW4gaWYgdGhlXG4gICAgICogcHJvbWlzZXMgcmV0dXJuZWQgZnJvbSBwcmV2aW91cyBjYWxscyB0byB7QGxpbmsgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyLndyaXRlIHwgd3JpdGUoKX0gaGF2ZSBub3QgeWV0IHNldHRsZWQpLlxuICAgICAqIEl04oCZcyBub3QgbmVjZXNzYXJ5IHRvIGhvbGQgdGhlIGxvY2sgb24gdGhlIHdyaXRlciBmb3IgdGhlIGR1cmF0aW9uIG9mIHRoZSB3cml0ZTsgdGhlIGxvY2sgaW5zdGVhZCBzaW1wbHkgcHJldmVudHNcbiAgICAgKiBvdGhlciBwcm9kdWNlcnMgZnJvbSB3cml0aW5nIGluIGFuIGludGVybGVhdmVkIG1hbm5lci5cbiAgICAgKi9cbiAgICByZWxlYXNlTG9jaygpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3JlbGVhc2VMb2NrJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyZWFtID0gdGhpcy5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICAgICAgaWYgKHN0cmVhbSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByZXR1cm47XG4gICAgICAgIH1cbiAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh0aGlzKTtcbiAgICB9XG4gICAgd3JpdGUoY2h1bmsgPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1dyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcih0aGlzKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoZGVmYXVsdFdyaXRlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ3dyaXRlJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0aGlzLl9vd25lcldyaXRhYmxlU3RyZWFtID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGRlZmF1bHRXcml0ZXJMb2NrRXhjZXB0aW9uKCd3cml0ZSB0bycpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyV3JpdGUodGhpcywgY2h1bmspO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUsIHtcbiAgICBhYm9ydDogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2U6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHJlbGVhc2VMb2NrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICB3cml0ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgY2xvc2VkOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBkZXNpcmVkU2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcmVhZHk6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyLlxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIoeCkge1xuICAgIGlmICghdHlwZUlzT2JqZWN0KHgpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgaWYgKCFPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoeCwgJ19vd25lcldyaXRhYmxlU3RyZWFtJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbi8vIEEgY2xpZW50IG9mIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciBtYXkgdXNlIHRoZXNlIGZ1bmN0aW9ucyBkaXJlY3RseSB0byBieXBhc3Mgc3RhdGUgY2hlY2suXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJBYm9ydCh3cml0ZXIsIHJlYXNvbikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1BYm9ydChzdHJlYW0sIHJlYXNvbik7XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh3cml0ZXIpIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgcmV0dXJuIFdyaXRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckNsb3NlV2l0aEVycm9yUHJvcGFnYXRpb24od3JpdGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gd3JpdGVyLl9vd25lcldyaXRhYmxlU3RyZWFtO1xuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoV3JpdGFibGVTdHJlYW1DbG9zZVF1ZXVlZE9ySW5GbGlnaHQoc3RyZWFtKSB8fCBzdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JlZCcpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxuICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZSh3cml0ZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlQ2xvc2VkUHJvbWlzZVJlamVjdGVkKHdyaXRlciwgZXJyb3IpIHtcbiAgICBpZiAod3JpdGVyLl9jbG9zZWRQcm9taXNlU3RhdGUgPT09ICdwZW5kaW5nJykge1xuICAgICAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCh3cml0ZXIsIGVycm9yKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgZXJyb3IpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZVJlYWR5UHJvbWlzZVJlamVjdGVkKHdyaXRlciwgZXJyb3IpIHtcbiAgICBpZiAod3JpdGVyLl9yZWFkeVByb21pc2VTdGF0ZSA9PT0gJ3BlbmRpbmcnKSB7XG4gICAgICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZWplY3Qod3JpdGVyLCBlcnJvcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgZXJyb3IpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckdldERlc2lyZWRTaXplKHdyaXRlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbTtcbiAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKHN0YXRlID09PSAnZXJyb3JlZCcgfHwgc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgcmV0dXJuIG51bGw7XG4gICAgfVxuICAgIGlmIChzdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgcmV0dXJuIDA7XG4gICAgfVxuICAgIHJldHVybiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUoc3RyZWFtLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh3cml0ZXIpIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgY29uc3QgcmVsZWFzZWRFcnJvciA9IG5ldyBUeXBlRXJyb3IoYFdyaXRlciB3YXMgcmVsZWFzZWQgYW5kIGNhbiBubyBsb25nZXIgYmUgdXNlZCB0byBtb25pdG9yIHRoZSBzdHJlYW0ncyBjbG9zZWRuZXNzYCk7XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyRW5zdXJlUmVhZHlQcm9taXNlUmVqZWN0ZWQod3JpdGVyLCByZWxlYXNlZEVycm9yKTtcbiAgICAvLyBUaGUgc3RhdGUgdHJhbnNpdGlvbnMgdG8gXCJlcnJvcmVkXCIgYmVmb3JlIHRoZSBzaW5rIGFib3J0KCkgbWV0aG9kIHJ1bnMsIGJ1dCB0aGUgd3JpdGVyLmNsb3NlZCBwcm9taXNlIGlzIG5vdFxuICAgIC8vIHJlamVjdGVkIHVudGlsIGFmdGVyd2FyZHMuIFRoaXMgbWVhbnMgdGhhdCBzaW1wbHkgdGVzdGluZyBzdGF0ZSB3aWxsIG5vdCB3b3JrLlxuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlckVuc3VyZUNsb3NlZFByb21pc2VSZWplY3RlZCh3cml0ZXIsIHJlbGVhc2VkRXJyb3IpO1xuICAgIHN0cmVhbS5fd3JpdGVyID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbSA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcldyaXRlKHdyaXRlciwgY2h1bmspIHtcbiAgICBjb25zdCBzdHJlYW0gPSB3cml0ZXIuX293bmVyV3JpdGFibGVTdHJlYW07XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIGNvbnN0IGNodW5rU2l6ZSA9IFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRDaHVua1NpemUoY29udHJvbGxlciwgY2h1bmspO1xuICAgIGlmIChzdHJlYW0gIT09IHdyaXRlci5fb3duZXJXcml0YWJsZVN0cmVhbSkge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbignd3JpdGUgdG8nKSk7XG4gICAgfVxuICAgIGNvbnN0IHN0YXRlID0gc3RyZWFtLl9zdGF0ZTtcbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmVkJykge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW0uX3N0b3JlZEVycm9yKTtcbiAgICB9XG4gICAgaWYgKFdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KHN0cmVhbSkgfHwgc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoJ1RoZSBzdHJlYW0gaXMgY2xvc2luZyBvciBjbG9zZWQgYW5kIGNhbm5vdCBiZSB3cml0dGVuIHRvJykpO1xuICAgIH1cbiAgICBpZiAoc3RhdGUgPT09ICdlcnJvcmluZycpIHtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoc3RyZWFtLl9zdG9yZWRFcnJvcik7XG4gICAgfVxuICAgIGNvbnN0IHByb21pc2UgPSBXcml0YWJsZVN0cmVhbUFkZFdyaXRlUmVxdWVzdChzdHJlYW0pO1xuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJXcml0ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKTtcbiAgICByZXR1cm4gcHJvbWlzZTtcbn1cbmNvbnN0IGNsb3NlU2VudGluZWwgPSB7fTtcbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgV3JpdGFibGVTdHJlYW0gfCB3cml0YWJsZSBzdHJlYW19J3Mgc3RhdGUgYW5kIGludGVybmFsIHF1ZXVlLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQ2xvc2VzIHRoZSBjb250cm9sbGVkIHdyaXRhYmxlIHN0cmVhbSwgbWFraW5nIGFsbCBmdXR1cmUgaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuXG4gICAgICpcbiAgICAgKiBUaGlzIG1ldGhvZCBpcyByYXJlbHkgdXNlZCwgc2luY2UgdXN1YWxseSBpdCBzdWZmaWNlcyB0byByZXR1cm4gYSByZWplY3RlZCBwcm9taXNlIGZyb20gb25lIG9mIHRoZSB1bmRlcmx5aW5nXG4gICAgICogc2luaydzIG1ldGhvZHMuIEhvd2V2ZXIsIGl0IGNhbiBiZSB1c2VmdWwgZm9yIHN1ZGRlbmx5IHNodXR0aW5nIGRvd24gYSBzdHJlYW0gaW4gcmVzcG9uc2UgdG8gYW4gZXZlbnQgb3V0c2lkZSB0aGVcbiAgICAgKiBub3JtYWwgbGlmZWN5Y2xlIG9mIGludGVyYWN0aW9ucyB3aXRoIHRoZSB1bmRlcmx5aW5nIHNpbmsuXG4gICAgICovXG4gICAgZXJyb3IoZSA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUuZXJyb3IgY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBzdGF0ZSA9IHRoaXMuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbS5fc3RhdGU7XG4gICAgICAgIGlmIChzdGF0ZSAhPT0gJ3dyaXRhYmxlJykge1xuICAgICAgICAgICAgLy8gVGhlIHN0cmVhbSBpcyBjbG9zZWQsIGVycm9yZWQgb3Igd2lsbCBiZSBzb29uLiBUaGUgc2luayBjYW4ndCBkbyBhbnl0aGluZyB1c2VmdWwgaWYgaXQgZ2V0cyBhbiBlcnJvciBoZXJlLCBzb1xuICAgICAgICAgICAgLy8ganVzdCB0cmVhdCBpdCBhcyBhIG5vLW9wLlxuICAgICAgICAgICAgcmV0dXJuO1xuICAgICAgICB9XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcih0aGlzLCBlKTtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtBYm9ydFN0ZXBzXShyZWFzb24pIHtcbiAgICAgICAgY29uc3QgcmVzdWx0ID0gdGhpcy5fYWJvcnRBbGdvcml0aG0ocmVhc29uKTtcbiAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyh0aGlzKTtcbiAgICAgICAgcmV0dXJuIHJlc3VsdDtcbiAgICB9XG4gICAgLyoqIEBpbnRlcm5hbCAqL1xuICAgIFtFcnJvclN0ZXBzXSgpIHtcbiAgICAgICAgUmVzZXRRdWV1ZSh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwge1xuICAgIGVycm9yOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGltcGxlbWVudGluZyBpbnRlcmZhY2UgcmVxdWlyZWQgYnkgdGhlIFdyaXRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gSXNXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIFNldFVwV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcihzdHJlYW0sIGNvbnRyb2xsZXIsIHN0YXJ0QWxnb3JpdGhtLCB3cml0ZUFsZ29yaXRobSwgY2xvc2VBbGdvcml0aG0sIGFib3J0QWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKSB7XG4gICAgY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtID0gc3RyZWFtO1xuICAgIHN0cmVhbS5fd3JpdGFibGVTdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICAvLyBOZWVkIHRvIHNldCB0aGUgc2xvdHMgc28gdGhhdCB0aGUgYXNzZXJ0IGRvZXNuJ3QgZmlyZS4gSW4gdGhlIHNwZWMgdGhlIHNsb3RzIGFscmVhZHkgZXhpc3QgaW1wbGljaXRseS5cbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA9IHVuZGVmaW5lZDtcbiAgICBSZXNldFF1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0gPSBzaXplQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNID0gaGlnaFdhdGVyTWFyaztcbiAgICBjb250cm9sbGVyLl93cml0ZUFsZ29yaXRobSA9IHdyaXRlQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX2Nsb3NlQWxnb3JpdGhtID0gY2xvc2VBbGdvcml0aG07XG4gICAgY29udHJvbGxlci5fYWJvcnRBbGdvcml0aG0gPSBhYm9ydEFsZ29yaXRobTtcbiAgICBjb25zdCBiYWNrcHJlc3N1cmUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0QmFja3ByZXNzdXJlKGNvbnRyb2xsZXIpO1xuICAgIFdyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKTtcbiAgICBjb25zdCBzdGFydFJlc3VsdCA9IHN0YXJ0QWxnb3JpdGhtKCk7XG4gICAgY29uc3Qgc3RhcnRQcm9taXNlID0gcHJvbWlzZVJlc29sdmVkV2l0aChzdGFydFJlc3VsdCk7XG4gICAgdXBvblByb21pc2Uoc3RhcnRQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSB0cnVlO1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQWR2YW5jZVF1ZXVlSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgfSwgciA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSB0cnVlO1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlYWxXaXRoUmVqZWN0aW9uKHN0cmVhbSwgcik7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBTZXRVcFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1Npbmsoc3RyZWFtLCB1bmRlcmx5aW5nU2luaywgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBsZXQgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlZmluZWQ7XG4gICAgbGV0IHdyaXRlQWxnb3JpdGhtID0gKCkgPT4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIGxldCBjbG9zZUFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBsZXQgYWJvcnRBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLnN0YXJ0ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlcmx5aW5nU2luay5zdGFydChjb250cm9sbGVyKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLndyaXRlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgd3JpdGVBbGdvcml0aG0gPSBjaHVuayA9PiB1bmRlcmx5aW5nU2luay53cml0ZShjaHVuaywgY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nU2luay5jbG9zZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGNsb3NlQWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ1NpbmsuY2xvc2UoKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTaW5rLmFib3J0ICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgYWJvcnRBbGdvcml0aG0gPSByZWFzb24gPT4gdW5kZXJseWluZ1NpbmsuYWJvcnQocmVhc29uKTtcbiAgICB9XG4gICAgU2V0VXBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHdyaXRlQWxnb3JpdGhtLCBjbG9zZUFsZ29yaXRobSwgYWJvcnRBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmssIHNpemVBbGdvcml0aG0pO1xufVxuLy8gQ2xlYXJBbGdvcml0aG1zIG1heSBiZSBjYWxsZWQgdHdpY2UuIEVycm9yaW5nIHRoZSBzYW1lIHN0cmVhbSBpbiBtdWx0aXBsZSB3YXlzIHdpbGwgb2Z0ZW4gcmVzdWx0IGluIHJlZHVuZGFudCBjYWxscy5cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcikge1xuICAgIGNvbnRyb2xsZXIuX3dyaXRlQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX2Nsb3NlQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX2Fib3J0QWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShjb250cm9sbGVyKSB7XG4gICAgRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udHJvbGxlciwgY2xvc2VTZW50aW5lbCwgMCk7XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldENodW5rU2l6ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIHRyeSB7XG4gICAgICAgIHJldHVybiBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0oY2h1bmspO1xuICAgIH1cbiAgICBjYXRjaCAoY2h1bmtTaXplRSkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBjaHVua1NpemVFKTtcbiAgICAgICAgcmV0dXJuIDE7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldERlc2lyZWRTaXplKGNvbnRyb2xsZXIpIHtcbiAgICByZXR1cm4gY29udHJvbGxlci5fc3RyYXRlZ3lIV00gLSBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZTtcbn1cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJXcml0ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKSB7XG4gICAgdHJ5IHtcbiAgICAgICAgRW5xdWV1ZVZhbHVlV2l0aFNpemUoY29udHJvbGxlciwgY2h1bmssIGNodW5rU2l6ZSk7XG4gICAgfVxuICAgIGNhdGNoIChlbnF1ZXVlRSkge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBlbnF1ZXVlRSk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtO1xuICAgIGlmICghV3JpdGFibGVTdHJlYW1DbG9zZVF1ZXVlZE9ySW5GbGlnaHQoc3RyZWFtKSAmJiBzdHJlYW0uX3N0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgIGNvbnN0IGJhY2twcmVzc3VyZSA9IFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXRCYWNrcHJlc3N1cmUoY29udHJvbGxlcik7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtVXBkYXRlQmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKTtcbiAgICB9XG4gICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckFkdmFuY2VRdWV1ZUlmTmVlZGVkKGNvbnRyb2xsZXIpO1xufVxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIuXG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQWR2YW5jZVF1ZXVlSWZOZWVkZWQoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBpZiAoIWNvbnRyb2xsZXIuX3N0YXJ0ZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoc3RyZWFtLl9pbkZsaWdodFdyaXRlUmVxdWVzdCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgY29uc3Qgc3RhdGUgPSBzdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ2Vycm9yaW5nJykge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEVycm9yaW5nKHN0cmVhbSk7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHZhbHVlID0gUGVla1F1ZXVlVmFsdWUoY29udHJvbGxlcik7XG4gICAgaWYgKHZhbHVlID09PSBjbG9zZVNlbnRpbmVsKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQcm9jZXNzQ2xvc2UoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGVsc2Uge1xuICAgICAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyUHJvY2Vzc1dyaXRlKGNvbnRyb2xsZXIsIHZhbHVlKTtcbiAgICB9XG59XG5mdW5jdGlvbiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChjb250cm9sbGVyLCBlcnJvcikge1xuICAgIGlmIChjb250cm9sbGVyLl9jb250cm9sbGVkV3JpdGFibGVTdHJlYW0uX3N0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlcnJvcik7XG4gICAgfVxufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NDbG9zZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFdyaXRhYmxlU3RyZWFtO1xuICAgIFdyaXRhYmxlU3RyZWFtTWFya0Nsb3NlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSk7XG4gICAgRGVxdWV1ZVZhbHVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IHNpbmtDbG9zZVByb21pc2UgPSBjb250cm9sbGVyLl9jbG9zZUFsZ29yaXRobSgpO1xuICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgdXBvblByb21pc2Uoc2lua0Nsb3NlUHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2Uoc3RyZWFtKTtcbiAgICB9LCByZWFzb24gPT4ge1xuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0Q2xvc2VXaXRoRXJyb3Ioc3RyZWFtLCByZWFzb24pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlclByb2Nlc3NXcml0ZShjb250cm9sbGVyLCBjaHVuaykge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBXcml0YWJsZVN0cmVhbU1hcmtGaXJzdFdyaXRlUmVxdWVzdEluRmxpZ2h0KHN0cmVhbSk7XG4gICAgY29uc3Qgc2lua1dyaXRlUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3dyaXRlQWxnb3JpdGhtKGNodW5rKTtcbiAgICB1cG9uUHJvbWlzZShzaW5rV3JpdGVQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRmluaXNoSW5GbGlnaHRXcml0ZShzdHJlYW0pO1xuICAgICAgICBjb25zdCBzdGF0ZSA9IHN0cmVhbS5fc3RhdGU7XG4gICAgICAgIERlcXVldWVWYWx1ZShjb250cm9sbGVyKTtcbiAgICAgICAgaWYgKCFXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChzdHJlYW0pICYmIHN0YXRlID09PSAnd3JpdGFibGUnKSB7XG4gICAgICAgICAgICBjb25zdCBiYWNrcHJlc3N1cmUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0QmFja3ByZXNzdXJlKGNvbnRyb2xsZXIpO1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1VcGRhdGVCYWNrcHJlc3N1cmUoc3RyZWFtLCBiYWNrcHJlc3N1cmUpO1xuICAgICAgICB9XG4gICAgICAgIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJBZHZhbmNlUXVldWVJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICB9LCByZWFzb24gPT4ge1xuICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ3dyaXRhYmxlJykge1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsZWFyQWxnb3JpdGhtcyhjb250cm9sbGVyKTtcbiAgICAgICAgfVxuICAgICAgICBXcml0YWJsZVN0cmVhbUZpbmlzaEluRmxpZ2h0V3JpdGVXaXRoRXJyb3Ioc3RyZWFtLCByZWFzb24pO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gV3JpdGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldEJhY2twcmVzc3VyZShjb250cm9sbGVyKSB7XG4gICAgY29uc3QgZGVzaXJlZFNpemUgPSBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUoY29udHJvbGxlcik7XG4gICAgcmV0dXJuIGRlc2lyZWRTaXplIDw9IDA7XG59XG4vLyBBIGNsaWVudCBvZiBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyIG1heSB1c2UgdGhlc2UgZnVuY3Rpb25zIGRpcmVjdGx5IHRvIGJ5cGFzcyBzdGF0ZSBjaGVjay5cbmZ1bmN0aW9uIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlcnJvcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRXcml0YWJsZVN0cmVhbTtcbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFdyaXRhYmxlU3RyZWFtU3RhcnRFcnJvcmluZyhzdHJlYW0sIGVycm9yKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbS5cbmZ1bmN0aW9uIHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24obmFtZSkge1xuICAgIHJldHVybiBuZXcgVHlwZUVycm9yKGBXcml0YWJsZVN0cmVhbS5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgV3JpdGFibGVTdHJlYW1gKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXIuXG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyYCk7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyTG9ja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoJ0Nhbm5vdCAnICsgbmFtZSArICcgYSBzdHJlYW0gdXNpbmcgYSByZWxlYXNlZCB3cml0ZXInKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpIHtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgd3JpdGVyLl9jbG9zZWRQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgICAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gcmVqZWN0O1xuICAgICAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VTdGF0ZSA9ICdwZW5kaW5nJztcbiAgICB9KTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pIHtcbiAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbiAgICBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlamVjdCh3cml0ZXIsIHJlYXNvbik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZUluaXRpYWxpemVBc1Jlc29sdmVkKHdyaXRlcikge1xuICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpO1xuICAgIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzb2x2ZSh3cml0ZXIpO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VSZWplY3Qod3JpdGVyLCByZWFzb24pIHtcbiAgICBpZiAod3JpdGVyLl9jbG9zZWRQcm9taXNlX3JlamVjdCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZSh3cml0ZXIuX2Nsb3NlZFByb21pc2UpO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QocmVhc29uKTtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVqZWN0ID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZVN0YXRlID0gJ3JlamVjdGVkJztcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJDbG9zZWRQcm9taXNlUmVzZXRUb1JlamVjdGVkKHdyaXRlciwgcmVhc29uKSB7XG4gICAgZGVmYXVsdFdyaXRlckNsb3NlZFByb21pc2VJbml0aWFsaXplQXNSZWplY3RlZCh3cml0ZXIsIHJlYXNvbik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyQ2xvc2VkUHJvbWlzZVJlc29sdmUod3JpdGVyKSB7XG4gICAgaWYgKHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB3cml0ZXIuX2Nsb3NlZFByb21pc2VfcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fY2xvc2VkUHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG4gICAgd3JpdGVyLl9jbG9zZWRQcm9taXNlU3RhdGUgPSAncmVzb2x2ZWQnO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKSB7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2UgPSBuZXdQcm9taXNlKChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICAgICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVzb2x2ZSA9IHJlc29sdmU7XG4gICAgICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9IHJlamVjdDtcbiAgICB9KTtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZVN0YXRlID0gJ3BlbmRpbmcnO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemVBc1JlamVjdGVkKHdyaXRlciwgcmVhc29uKSB7XG4gICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVqZWN0KHdyaXRlciwgcmVhc29uKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VJbml0aWFsaXplQXNSZXNvbHZlZCh3cml0ZXIpIHtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZSh3cml0ZXIpO1xuICAgIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNvbHZlKHdyaXRlcik7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVqZWN0KHdyaXRlciwgcmVhc29uKSB7XG4gICAgaWYgKHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZSh3cml0ZXIuX3JlYWR5UHJvbWlzZSk7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVqZWN0KHJlYXNvbik7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZV9yZWplY3QgPSB1bmRlZmluZWQ7XG4gICAgd3JpdGVyLl9yZWFkeVByb21pc2VTdGF0ZSA9ICdyZWplY3RlZCc7XG59XG5mdW5jdGlvbiBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlUmVzZXQod3JpdGVyKSB7XG4gICAgZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZUluaXRpYWxpemUod3JpdGVyKTtcbn1cbmZ1bmN0aW9uIGRlZmF1bHRXcml0ZXJSZWFkeVByb21pc2VSZXNldFRvUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pIHtcbiAgICBkZWZhdWx0V3JpdGVyUmVhZHlQcm9taXNlSW5pdGlhbGl6ZUFzUmVqZWN0ZWQod3JpdGVyLCByZWFzb24pO1xufVxuZnVuY3Rpb24gZGVmYXVsdFdyaXRlclJlYWR5UHJvbWlzZVJlc29sdmUod3JpdGVyKSB7XG4gICAgaWYgKHdyaXRlci5fcmVhZHlQcm9taXNlX3Jlc29sdmUgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3Jlc29sdmUodW5kZWZpbmVkKTtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZV9yZXNvbHZlID0gdW5kZWZpbmVkO1xuICAgIHdyaXRlci5fcmVhZHlQcm9taXNlX3JlamVjdCA9IHVuZGVmaW5lZDtcbiAgICB3cml0ZXIuX3JlYWR5UHJvbWlzZVN0YXRlID0gJ2Z1bGZpbGxlZCc7XG59XG5cbmZ1bmN0aW9uIGlzQWJvcnRTaWduYWwodmFsdWUpIHtcbiAgICBpZiAodHlwZW9mIHZhbHVlICE9PSAnb2JqZWN0JyB8fCB2YWx1ZSA9PT0gbnVsbCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIHJldHVybiB0eXBlb2YgdmFsdWUuYWJvcnRlZCA9PT0gJ2Jvb2xlYW4nO1xuICAgIH1cbiAgICBjYXRjaCAoX2EpIHtcbiAgICAgICAgLy8gQWJvcnRTaWduYWwucHJvdG90eXBlLmFib3J0ZWQgdGhyb3dzIGlmIGl0cyBicmFuZCBjaGVjayBmYWlsc1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxufVxuXG4vLy8gPHJlZmVyZW5jZSBsaWI9XCJkb21cIiAvPlxuY29uc3QgTmF0aXZlRE9NRXhjZXB0aW9uID0gdHlwZW9mIERPTUV4Y2VwdGlvbiAhPT0gJ3VuZGVmaW5lZCcgPyBET01FeGNlcHRpb24gOiB1bmRlZmluZWQ7XG5cbi8vLyA8cmVmZXJlbmNlIHR5cGVzPVwibm9kZVwiIC8+XG5mdW5jdGlvbiBpc0RPTUV4Y2VwdGlvbkNvbnN0cnVjdG9yKGN0b3IpIHtcbiAgICBpZiAoISh0eXBlb2YgY3RvciA9PT0gJ2Z1bmN0aW9uJyB8fCB0eXBlb2YgY3RvciA9PT0gJ29iamVjdCcpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgbmV3IGN0b3IoKTtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIGNhdGNoIChfYSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxufVxuZnVuY3Rpb24gY3JlYXRlRE9NRXhjZXB0aW9uUG9seWZpbGwoKSB7XG4gICAgY29uc3QgY3RvciA9IGZ1bmN0aW9uIERPTUV4Y2VwdGlvbihtZXNzYWdlLCBuYW1lKSB7XG4gICAgICAgIHRoaXMubWVzc2FnZSA9IG1lc3NhZ2UgfHwgJyc7XG4gICAgICAgIHRoaXMubmFtZSA9IG5hbWUgfHwgJ0Vycm9yJztcbiAgICAgICAgaWYgKEVycm9yLmNhcHR1cmVTdGFja1RyYWNlKSB7XG4gICAgICAgICAgICBFcnJvci5jYXB0dXJlU3RhY2tUcmFjZSh0aGlzLCB0aGlzLmNvbnN0cnVjdG9yKTtcbiAgICAgICAgfVxuICAgIH07XG4gICAgY3Rvci5wcm90b3R5cGUgPSBPYmplY3QuY3JlYXRlKEVycm9yLnByb3RvdHlwZSk7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KGN0b3IucHJvdG90eXBlLCAnY29uc3RydWN0b3InLCB7IHZhbHVlOiBjdG9yLCB3cml0YWJsZTogdHJ1ZSwgY29uZmlndXJhYmxlOiB0cnVlIH0pO1xuICAgIHJldHVybiBjdG9yO1xufVxuY29uc3QgRE9NRXhjZXB0aW9uJDEgPSBpc0RPTUV4Y2VwdGlvbkNvbnN0cnVjdG9yKE5hdGl2ZURPTUV4Y2VwdGlvbikgPyBOYXRpdmVET01FeGNlcHRpb24gOiBjcmVhdGVET01FeGNlcHRpb25Qb2x5ZmlsbCgpO1xuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVBpcGVUbyhzb3VyY2UsIGRlc3QsIHByZXZlbnRDbG9zZSwgcHJldmVudEFib3J0LCBwcmV2ZW50Q2FuY2VsLCBzaWduYWwpIHtcbiAgICBjb25zdCByZWFkZXIgPSBBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHNvdXJjZSk7XG4gICAgY29uc3Qgd3JpdGVyID0gQWNxdWlyZVdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlcihkZXN0KTtcbiAgICBzb3VyY2UuX2Rpc3R1cmJlZCA9IHRydWU7XG4gICAgbGV0IHNodXR0aW5nRG93biA9IGZhbHNlO1xuICAgIC8vIFRoaXMgaXMgdXNlZCB0byBrZWVwIHRyYWNrIG9mIHRoZSBzcGVjJ3MgcmVxdWlyZW1lbnQgdGhhdCB3ZSB3YWl0IGZvciBvbmdvaW5nIHdyaXRlcyBkdXJpbmcgc2h1dGRvd24uXG4gICAgbGV0IGN1cnJlbnRXcml0ZSA9IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICByZXR1cm4gbmV3UHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgICAgIGxldCBhYm9ydEFsZ29yaXRobTtcbiAgICAgICAgaWYgKHNpZ25hbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBhYm9ydEFsZ29yaXRobSA9ICgpID0+IHtcbiAgICAgICAgICAgICAgICBjb25zdCBlcnJvciA9IG5ldyBET01FeGNlcHRpb24kMSgnQWJvcnRlZCcsICdBYm9ydEVycm9yJyk7XG4gICAgICAgICAgICAgICAgY29uc3QgYWN0aW9ucyA9IFtdO1xuICAgICAgICAgICAgICAgIGlmICghcHJldmVudEFib3J0KSB7XG4gICAgICAgICAgICAgICAgICAgIGFjdGlvbnMucHVzaCgoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoZGVzdC5fc3RhdGUgPT09ICd3cml0YWJsZScpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4gV3JpdGFibGVTdHJlYW1BYm9ydChkZXN0LCBlcnJvcik7XG4gICAgICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgaWYgKCFwcmV2ZW50Q2FuY2VsKSB7XG4gICAgICAgICAgICAgICAgICAgIGFjdGlvbnMucHVzaCgoKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBpZiAoc291cmNlLl9zdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbUNhbmNlbChzb3VyY2UsIGVycm9yKTtcbiAgICAgICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBzaHV0ZG93bldpdGhBY3Rpb24oKCkgPT4gUHJvbWlzZS5hbGwoYWN0aW9ucy5tYXAoYWN0aW9uID0+IGFjdGlvbigpKSksIHRydWUsIGVycm9yKTtcbiAgICAgICAgICAgIH07XG4gICAgICAgICAgICBpZiAoc2lnbmFsLmFib3J0ZWQpIHtcbiAgICAgICAgICAgICAgICBhYm9ydEFsZ29yaXRobSgpO1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHNpZ25hbC5hZGRFdmVudExpc3RlbmVyKCdhYm9ydCcsIGFib3J0QWxnb3JpdGhtKTtcbiAgICAgICAgfVxuICAgICAgICAvLyBVc2luZyByZWFkZXIgYW5kIHdyaXRlciwgcmVhZCBhbGwgY2h1bmtzIGZyb20gdGhpcyBhbmQgd3JpdGUgdGhlbSB0byBkZXN0XG4gICAgICAgIC8vIC0gQmFja3ByZXNzdXJlIG11c3QgYmUgZW5mb3JjZWRcbiAgICAgICAgLy8gLSBTaHV0ZG93biBtdXN0IHN0b3AgYWxsIGFjdGl2aXR5XG4gICAgICAgIGZ1bmN0aW9uIHBpcGVMb29wKCkge1xuICAgICAgICAgICAgcmV0dXJuIG5ld1Byb21pc2UoKHJlc29sdmVMb29wLCByZWplY3RMb29wKSA9PiB7XG4gICAgICAgICAgICAgICAgZnVuY3Rpb24gbmV4dChkb25lKSB7XG4gICAgICAgICAgICAgICAgICAgIGlmIChkb25lKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICByZXNvbHZlTG9vcCgpO1xuICAgICAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgICAgICAgICAgLy8gVXNlIGBQZXJmb3JtUHJvbWlzZVRoZW5gIGluc3RlYWQgb2YgYHVwb25Qcm9taXNlYCB0byBhdm9pZFxuICAgICAgICAgICAgICAgICAgICAgICAgLy8gYWRkaW5nIHVubmVjZXNzYXJ5IGAuY2F0Y2gocmV0aHJvd0Fzc2VydGlvbkVycm9yUmVqZWN0aW9uKWAgaGFuZGxlcnNcbiAgICAgICAgICAgICAgICAgICAgICAgIFBlcmZvcm1Qcm9taXNlVGhlbihwaXBlU3RlcCgpLCBuZXh0LCByZWplY3RMb29wKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBuZXh0KGZhbHNlKTtcbiAgICAgICAgICAgIH0pO1xuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIHBpcGVTdGVwKCkge1xuICAgICAgICAgICAgaWYgKHNodXR0aW5nRG93bikge1xuICAgICAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHRydWUpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIFBlcmZvcm1Qcm9taXNlVGhlbih3cml0ZXIuX3JlYWR5UHJvbWlzZSwgKCkgPT4ge1xuICAgICAgICAgICAgICAgIHJldHVybiBuZXdQcm9taXNlKChyZXNvbHZlUmVhZCwgcmVqZWN0UmVhZCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXJSZWFkKHJlYWRlciwge1xuICAgICAgICAgICAgICAgICAgICAgICAgX2NodW5rU3RlcHM6IGNodW5rID0+IHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBjdXJyZW50V3JpdGUgPSBQZXJmb3JtUHJvbWlzZVRoZW4oV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyV3JpdGUod3JpdGVyLCBjaHVuayksIHVuZGVmaW5lZCwgbm9vcCk7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVzb2x2ZVJlYWQoZmFsc2UpO1xuICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiByZXNvbHZlUmVhZCh0cnVlKSxcbiAgICAgICAgICAgICAgICAgICAgICAgIF9lcnJvclN0ZXBzOiByZWplY3RSZWFkXG4gICAgICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgfSk7XG4gICAgICAgIH1cbiAgICAgICAgLy8gRXJyb3JzIG11c3QgYmUgcHJvcGFnYXRlZCBmb3J3YXJkXG4gICAgICAgIGlzT3JCZWNvbWVzRXJyb3JlZChzb3VyY2UsIHJlYWRlci5fY2xvc2VkUHJvbWlzZSwgc3RvcmVkRXJyb3IgPT4ge1xuICAgICAgICAgICAgaWYgKCFwcmV2ZW50QWJvcnQpIHtcbiAgICAgICAgICAgICAgICBzaHV0ZG93bldpdGhBY3Rpb24oKCkgPT4gV3JpdGFibGVTdHJlYW1BYm9ydChkZXN0LCBzdG9yZWRFcnJvciksIHRydWUsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHNodXRkb3duKHRydWUsIHN0b3JlZEVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG4gICAgICAgIC8vIEVycm9ycyBtdXN0IGJlIHByb3BhZ2F0ZWQgYmFja3dhcmRcbiAgICAgICAgaXNPckJlY29tZXNFcnJvcmVkKGRlc3QsIHdyaXRlci5fY2xvc2VkUHJvbWlzZSwgc3RvcmVkRXJyb3IgPT4ge1xuICAgICAgICAgICAgaWYgKCFwcmV2ZW50Q2FuY2VsKSB7XG4gICAgICAgICAgICAgICAgc2h1dGRvd25XaXRoQWN0aW9uKCgpID0+IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHNvdXJjZSwgc3RvcmVkRXJyb3IpLCB0cnVlLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgICAgICBzaHV0ZG93bih0cnVlLCBzdG9yZWRFcnJvcik7XG4gICAgICAgICAgICB9XG4gICAgICAgIH0pO1xuICAgICAgICAvLyBDbG9zaW5nIG11c3QgYmUgcHJvcGFnYXRlZCBmb3J3YXJkXG4gICAgICAgIGlzT3JCZWNvbWVzQ2xvc2VkKHNvdXJjZSwgcmVhZGVyLl9jbG9zZWRQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgICAgICBpZiAoIXByZXZlbnRDbG9zZSkge1xuICAgICAgICAgICAgICAgIHNodXRkb3duV2l0aEFjdGlvbigoKSA9PiBXcml0YWJsZVN0cmVhbURlZmF1bHRXcml0ZXJDbG9zZVdpdGhFcnJvclByb3BhZ2F0aW9uKHdyaXRlcikpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgc2h1dGRvd24oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfSk7XG4gICAgICAgIC8vIENsb3NpbmcgbXVzdCBiZSBwcm9wYWdhdGVkIGJhY2t3YXJkXG4gICAgICAgIGlmIChXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChkZXN0KSB8fCBkZXN0Ll9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgICAgIGNvbnN0IGRlc3RDbG9zZWQgPSBuZXcgVHlwZUVycm9yKCd0aGUgZGVzdGluYXRpb24gd3JpdGFibGUgc3RyZWFtIGNsb3NlZCBiZWZvcmUgYWxsIGRhdGEgY291bGQgYmUgcGlwZWQgdG8gaXQnKTtcbiAgICAgICAgICAgIGlmICghcHJldmVudENhbmNlbCkge1xuICAgICAgICAgICAgICAgIHNodXRkb3duV2l0aEFjdGlvbigoKSA9PiBSZWFkYWJsZVN0cmVhbUNhbmNlbChzb3VyY2UsIGRlc3RDbG9zZWQpLCB0cnVlLCBkZXN0Q2xvc2VkKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHNodXRkb3duKHRydWUsIGRlc3RDbG9zZWQpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIHNldFByb21pc2VJc0hhbmRsZWRUb1RydWUocGlwZUxvb3AoKSk7XG4gICAgICAgIGZ1bmN0aW9uIHdhaXRGb3JXcml0ZXNUb0ZpbmlzaCgpIHtcbiAgICAgICAgICAgIC8vIEFub3RoZXIgd3JpdGUgbWF5IGhhdmUgc3RhcnRlZCB3aGlsZSB3ZSB3ZXJlIHdhaXRpbmcgb24gdGhpcyBjdXJyZW50V3JpdGUsIHNvIHdlIGhhdmUgdG8gYmUgc3VyZSB0byB3YWl0XG4gICAgICAgICAgICAvLyBmb3IgdGhhdCB0b28uXG4gICAgICAgICAgICBjb25zdCBvbGRDdXJyZW50V3JpdGUgPSBjdXJyZW50V3JpdGU7XG4gICAgICAgICAgICByZXR1cm4gUGVyZm9ybVByb21pc2VUaGVuKGN1cnJlbnRXcml0ZSwgKCkgPT4gb2xkQ3VycmVudFdyaXRlICE9PSBjdXJyZW50V3JpdGUgPyB3YWl0Rm9yV3JpdGVzVG9GaW5pc2goKSA6IHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgZnVuY3Rpb24gaXNPckJlY29tZXNFcnJvcmVkKHN0cmVhbSwgcHJvbWlzZSwgYWN0aW9uKSB7XG4gICAgICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgICAgICAgICAgYWN0aW9uKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgdXBvblJlamVjdGlvbihwcm9taXNlLCBhY3Rpb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIGlzT3JCZWNvbWVzQ2xvc2VkKHN0cmVhbSwgcHJvbWlzZSwgYWN0aW9uKSB7XG4gICAgICAgICAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Nsb3NlZCcpIHtcbiAgICAgICAgICAgICAgICBhY3Rpb24oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIHVwb25GdWxmaWxsbWVudChwcm9taXNlLCBhY3Rpb24pO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIGZ1bmN0aW9uIHNodXRkb3duV2l0aEFjdGlvbihhY3Rpb24sIG9yaWdpbmFsSXNFcnJvciwgb3JpZ2luYWxFcnJvcikge1xuICAgICAgICAgICAgaWYgKHNodXR0aW5nRG93bikge1xuICAgICAgICAgICAgICAgIHJldHVybjtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIHNodXR0aW5nRG93biA9IHRydWU7XG4gICAgICAgICAgICBpZiAoZGVzdC5fc3RhdGUgPT09ICd3cml0YWJsZScgJiYgIVdyaXRhYmxlU3RyZWFtQ2xvc2VRdWV1ZWRPckluRmxpZ2h0KGRlc3QpKSB7XG4gICAgICAgICAgICAgICAgdXBvbkZ1bGZpbGxtZW50KHdhaXRGb3JXcml0ZXNUb0ZpbmlzaCgpLCBkb1RoZVJlc3QpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgZG9UaGVSZXN0KCk7XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBmdW5jdGlvbiBkb1RoZVJlc3QoKSB7XG4gICAgICAgICAgICAgICAgdXBvblByb21pc2UoYWN0aW9uKCksICgpID0+IGZpbmFsaXplKG9yaWdpbmFsSXNFcnJvciwgb3JpZ2luYWxFcnJvciksIG5ld0Vycm9yID0+IGZpbmFsaXplKHRydWUsIG5ld0Vycm9yKSk7XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgICAgZnVuY3Rpb24gc2h1dGRvd24oaXNFcnJvciwgZXJyb3IpIHtcbiAgICAgICAgICAgIGlmIChzaHV0dGluZ0Rvd24pIHtcbiAgICAgICAgICAgICAgICByZXR1cm47XG4gICAgICAgICAgICB9XG4gICAgICAgICAgICBzaHV0dGluZ0Rvd24gPSB0cnVlO1xuICAgICAgICAgICAgaWYgKGRlc3QuX3N0YXRlID09PSAnd3JpdGFibGUnICYmICFXcml0YWJsZVN0cmVhbUNsb3NlUXVldWVkT3JJbkZsaWdodChkZXN0KSkge1xuICAgICAgICAgICAgICAgIHVwb25GdWxmaWxsbWVudCh3YWl0Rm9yV3JpdGVzVG9GaW5pc2goKSwgKCkgPT4gZmluYWxpemUoaXNFcnJvciwgZXJyb3IpKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIGZpbmFsaXplKGlzRXJyb3IsIGVycm9yKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICBmdW5jdGlvbiBmaW5hbGl6ZShpc0Vycm9yLCBlcnJvcikge1xuICAgICAgICAgICAgV3JpdGFibGVTdHJlYW1EZWZhdWx0V3JpdGVyUmVsZWFzZSh3cml0ZXIpO1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1SZWFkZXJHZW5lcmljUmVsZWFzZShyZWFkZXIpO1xuICAgICAgICAgICAgaWYgKHNpZ25hbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICAgICAgc2lnbmFsLnJlbW92ZUV2ZW50TGlzdGVuZXIoJ2Fib3J0JywgYWJvcnRBbGdvcml0aG0pO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgaWYgKGlzRXJyb3IpIHtcbiAgICAgICAgICAgICAgICByZWplY3QoZXJyb3IpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICAgICAgcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgICAgICAgICAgfVxuICAgICAgICB9XG4gICAgfSk7XG59XG5cbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgYSB7QGxpbmsgUmVhZGFibGVTdHJlYW0gfCByZWFkYWJsZSBzdHJlYW19J3Mgc3RhdGUgYW5kIGludGVybmFsIHF1ZXVlLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIGNvbnRyb2xsZWQgc3RyZWFtJ3MgaW50ZXJuYWwgcXVldWUuIEl0IGNhbiBiZSBuZWdhdGl2ZSwgaWYgdGhlIHF1ZXVlIGlzXG4gICAgICogb3Zlci1mdWxsLiBBbiB1bmRlcmx5aW5nIHNvdXJjZSBvdWdodCB0byB1c2UgdGhpcyBpbmZvcm1hdGlvbiB0byBkZXRlcm1pbmUgd2hlbiBhbmQgaG93IHRvIGFwcGx5IGJhY2twcmVzc3VyZS5cbiAgICAgKi9cbiAgICBnZXQgZGVzaXJlZFNpemUoKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Rlc2lyZWRTaXplJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZSh0aGlzKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogQ2xvc2VzIHRoZSBjb250cm9sbGVkIHJlYWRhYmxlIHN0cmVhbS4gQ29uc3VtZXJzIHdpbGwgc3RpbGwgYmUgYWJsZSB0byByZWFkIGFueSBwcmV2aW91c2x5LWVucXVldWVkIGNodW5rcyBmcm9tXG4gICAgICogdGhlIHN0cmVhbSwgYnV0IG9uY2UgdGhvc2UgYXJlIHJlYWQsIHRoZSBzdHJlYW0gd2lsbCBiZWNvbWUgY2xvc2VkLlxuICAgICAqL1xuICAgIGNsb3NlKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdjbG9zZScpO1xuICAgICAgICB9XG4gICAgICAgIGlmICghUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbkNsb3NlT3JFbnF1ZXVlKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgc3RyZWFtIGlzIG5vdCBpbiBhIHN0YXRlIHRoYXQgcGVybWl0cyBjbG9zZScpO1xuICAgICAgICB9XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZSh0aGlzKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuayA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKCFSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FuQ2xvc2VPckVucXVldWUodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1RoZSBzdHJlYW0gaXMgbm90IGluIGEgc3RhdGUgdGhhdCBwZXJtaXRzIGVucXVldWUnKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUodGhpcywgY2h1bmspO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBFcnJvcnMgdGhlIGNvbnRyb2xsZWQgcmVhZGFibGUgc3RyZWFtLCBtYWtpbmcgYWxsIGZ1dHVyZSBpbnRlcmFjdGlvbnMgd2l0aCBpdCBmYWlsIHdpdGggdGhlIGdpdmVuIGVycm9yIGBlYC5cbiAgICAgKi9cbiAgICBlcnJvcihlID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24oJ2Vycm9yJyk7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKHRoaXMsIGUpO1xuICAgIH1cbiAgICAvKiogQGludGVybmFsICovXG4gICAgW0NhbmNlbFN0ZXBzXShyZWFzb24pIHtcbiAgICAgICAgUmVzZXRRdWV1ZSh0aGlzKTtcbiAgICAgICAgY29uc3QgcmVzdWx0ID0gdGhpcy5fY2FuY2VsQWxnb3JpdGhtKHJlYXNvbik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXModGhpcyk7XG4gICAgICAgIHJldHVybiByZXN1bHQ7XG4gICAgfVxuICAgIC8qKiBAaW50ZXJuYWwgKi9cbiAgICBbUHVsbFN0ZXBzXShyZWFkUmVxdWVzdCkge1xuICAgICAgICBjb25zdCBzdHJlYW0gPSB0aGlzLl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW07XG4gICAgICAgIGlmICh0aGlzLl9xdWV1ZS5sZW5ndGggPiAwKSB7XG4gICAgICAgICAgICBjb25zdCBjaHVuayA9IERlcXVldWVWYWx1ZSh0aGlzKTtcbiAgICAgICAgICAgIGlmICh0aGlzLl9jbG9zZVJlcXVlc3RlZCAmJiB0aGlzLl9xdWV1ZS5sZW5ndGggPT09IDApIHtcbiAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKHRoaXMpO1xuICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKHRoaXMpO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2NodW5rU3RlcHMoY2h1bmspO1xuICAgICAgICB9XG4gICAgICAgIGVsc2Uge1xuICAgICAgICAgICAgUmVhZGFibGVTdHJlYW1BZGRSZWFkUmVxdWVzdChzdHJlYW0sIHJlYWRSZXF1ZXN0KTtcbiAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYWxsUHVsbElmTmVlZGVkKHRoaXMpO1xuICAgICAgICB9XG4gICAgfVxufVxuT2JqZWN0LmRlZmluZVByb3BlcnRpZXMoUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBjbG9zZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgZW5xdWV1ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgZXJyb3I6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGRlc2lyZWRTaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcsIHtcbiAgICAgICAgdmFsdWU6ICdSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBBYnN0cmFjdCBvcGVyYXRpb25zIGZvciB0aGUgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcih4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbScpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc2hvdWxkUHVsbCA9IFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKTtcbiAgICBpZiAoIXNob3VsZFB1bGwpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICBpZiAoY29udHJvbGxlci5fcHVsbGluZykge1xuICAgICAgICBjb250cm9sbGVyLl9wdWxsQWdhaW4gPSB0cnVlO1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSB0cnVlO1xuICAgIGNvbnN0IHB1bGxQcm9taXNlID0gY29udHJvbGxlci5fcHVsbEFsZ29yaXRobSgpO1xuICAgIHVwb25Qcm9taXNlKHB1bGxQcm9taXNlLCAoKSA9PiB7XG4gICAgICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSBmYWxzZTtcbiAgICAgICAgaWYgKGNvbnRyb2xsZXIuX3B1bGxBZ2Fpbikge1xuICAgICAgICAgICAgY29udHJvbGxlci5fcHVsbEFnYWluID0gZmFsc2U7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbiAgICAgICAgfVxuICAgIH0sIGUgPT4ge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSk7XG4gICAgfSk7XG59XG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyU2hvdWxkQ2FsbFB1bGwoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghY29udHJvbGxlci5fc3RhcnRlZCkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHN0cmVhbSkgJiYgUmVhZGFibGVTdHJlYW1HZXROdW1SZWFkUmVxdWVzdHMoc3RyZWFtKSA+IDApIHtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIGNvbnN0IGRlc2lyZWRTaXplID0gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckdldERlc2lyZWRTaXplKGNvbnRyb2xsZXIpO1xuICAgIGlmIChkZXNpcmVkU2l6ZSA+IDApIHtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgfVxuICAgIHJldHVybiBmYWxzZTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcikge1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fY2FuY2VsQWxnb3JpdGhtID0gdW5kZWZpbmVkO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5U2l6ZUFsZ29yaXRobSA9IHVuZGVmaW5lZDtcbn1cbi8vIEEgY2xpZW50IG9mIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIgbWF5IHVzZSB0aGVzZSBmdW5jdGlvbnMgZGlyZWN0bHkgdG8gYnlwYXNzIHN0YXRlIGNoZWNrLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsb3NlKGNvbnRyb2xsZXIpIHtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCA9IHRydWU7XG4gICAgaWYgKGNvbnRyb2xsZXIuX3F1ZXVlLmxlbmd0aCA9PT0gMCkge1xuICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgICAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG4gICAgfVxufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUoY29udHJvbGxlciwgY2h1bmspIHtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbTtcbiAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZChzdHJlYW0pICYmIFJlYWRhYmxlU3RyZWFtR2V0TnVtUmVhZFJlcXVlc3RzKHN0cmVhbSkgPiAwKSB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRnVsZmlsbFJlYWRSZXF1ZXN0KHN0cmVhbSwgY2h1bmssIGZhbHNlKTtcbiAgICB9XG4gICAgZWxzZSB7XG4gICAgICAgIGxldCBjaHVua1NpemU7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBjaHVua1NpemUgPSBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0oY2h1bmspO1xuICAgICAgICB9XG4gICAgICAgIGNhdGNoIChjaHVua1NpemVFKSB7XG4gICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgY2h1bmtTaXplRSk7XG4gICAgICAgICAgICB0aHJvdyBjaHVua1NpemVFO1xuICAgICAgICB9XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBFbnF1ZXVlVmFsdWVXaXRoU2l6ZShjb250cm9sbGVyLCBjaHVuaywgY2h1bmtTaXplKTtcbiAgICAgICAgfVxuICAgICAgICBjYXRjaCAoZW5xdWV1ZUUpIHtcbiAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlbnF1ZXVlRSk7XG4gICAgICAgICAgICB0aHJvdyBlbnF1ZXVlRTtcbiAgICAgICAgfVxuICAgIH1cbiAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2FsbFB1bGxJZk5lZWRlZChjb250cm9sbGVyKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCBlKSB7XG4gICAgY29uc3Qgc3RyZWFtID0gY29udHJvbGxlci5fY29udHJvbGxlZFJlYWRhYmxlU3RyZWFtO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlICE9PSAncmVhZGFibGUnKSB7XG4gICAgICAgIHJldHVybjtcbiAgICB9XG4gICAgUmVzZXRRdWV1ZShjb250cm9sbGVyKTtcbiAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpO1xuICAgIFJlYWRhYmxlU3RyZWFtRXJyb3Ioc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJHZXREZXNpcmVkU2l6ZShjb250cm9sbGVyKSB7XG4gICAgY29uc3Qgc3RhdGUgPSBjb250cm9sbGVyLl9jb250cm9sbGVkUmVhZGFibGVTdHJlYW0uX3N0YXRlO1xuICAgIGlmIChzdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBudWxsO1xuICAgIH1cbiAgICBpZiAoc3RhdGUgPT09ICdjbG9zZWQnKSB7XG4gICAgICAgIHJldHVybiAwO1xuICAgIH1cbiAgICByZXR1cm4gY29udHJvbGxlci5fc3RyYXRlZ3lIV00gLSBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZTtcbn1cbi8vIFRoaXMgaXMgdXNlZCBpbiB0aGUgaW1wbGVtZW50YXRpb24gb2YgVHJhbnNmb3JtU3RyZWFtLlxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZShjb250cm9sbGVyKSB7XG4gICAgaWYgKFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJTaG91bGRDYWxsUHVsbChjb250cm9sbGVyKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbkNsb3NlT3JFbnF1ZXVlKGNvbnRyb2xsZXIpIHtcbiAgICBjb25zdCBzdGF0ZSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbS5fc3RhdGU7XG4gICAgaWYgKCFjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCAmJiBzdGF0ZSA9PT0gJ3JlYWRhYmxlJykge1xuICAgICAgICByZXR1cm4gdHJ1ZTtcbiAgICB9XG4gICAgcmV0dXJuIGZhbHNlO1xufVxuZnVuY3Rpb24gU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHB1bGxBbGdvcml0aG0sIGNhbmNlbEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRSZWFkYWJsZVN0cmVhbSA9IHN0cmVhbTtcbiAgICBjb250cm9sbGVyLl9xdWV1ZSA9IHVuZGVmaW5lZDtcbiAgICBjb250cm9sbGVyLl9xdWV1ZVRvdGFsU2l6ZSA9IHVuZGVmaW5lZDtcbiAgICBSZXNldFF1ZXVlKGNvbnRyb2xsZXIpO1xuICAgIGNvbnRyb2xsZXIuX3N0YXJ0ZWQgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9jbG9zZVJlcXVlc3RlZCA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxBZ2FpbiA9IGZhbHNlO1xuICAgIGNvbnRyb2xsZXIuX3B1bGxpbmcgPSBmYWxzZTtcbiAgICBjb250cm9sbGVyLl9zdHJhdGVneVNpemVBbGdvcml0aG0gPSBzaXplQWxnb3JpdGhtO1xuICAgIGNvbnRyb2xsZXIuX3N0cmF0ZWd5SFdNID0gaGlnaFdhdGVyTWFyaztcbiAgICBjb250cm9sbGVyLl9wdWxsQWxnb3JpdGhtID0gcHVsbEFsZ29yaXRobTtcbiAgICBjb250cm9sbGVyLl9jYW5jZWxBbGdvcml0aG0gPSBjYW5jZWxBbGdvcml0aG07XG4gICAgc3RyZWFtLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIgPSBjb250cm9sbGVyO1xuICAgIGNvbnN0IHN0YXJ0UmVzdWx0ID0gc3RhcnRBbGdvcml0aG0oKTtcbiAgICB1cG9uUHJvbWlzZShwcm9taXNlUmVzb2x2ZWRXaXRoKHN0YXJ0UmVzdWx0KSwgKCkgPT4ge1xuICAgICAgICBjb250cm9sbGVyLl9zdGFydGVkID0gdHJ1ZTtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNhbGxQdWxsSWZOZWVkZWQoY29udHJvbGxlcik7XG4gICAgfSwgciA9PiB7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihjb250cm9sbGVyLCByKTtcbiAgICB9KTtcbn1cbmZ1bmN0aW9uIFNldFVwUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckZyb21VbmRlcmx5aW5nU291cmNlKHN0cmVhbSwgdW5kZXJseWluZ1NvdXJjZSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSkge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlKTtcbiAgICBsZXQgc3RhcnRBbGdvcml0aG0gPSAoKSA9PiB1bmRlZmluZWQ7XG4gICAgbGV0IHB1bGxBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgbGV0IGNhbmNlbEFsZ29yaXRobSA9ICgpID0+IHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICBpZiAodW5kZXJseWluZ1NvdXJjZS5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHN0YXJ0QWxnb3JpdGhtID0gKCkgPT4gdW5kZXJseWluZ1NvdXJjZS5zdGFydChjb250cm9sbGVyKTtcbiAgICB9XG4gICAgaWYgKHVuZGVybHlpbmdTb3VyY2UucHVsbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHB1bGxBbGdvcml0aG0gPSAoKSA9PiB1bmRlcmx5aW5nU291cmNlLnB1bGwoY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh1bmRlcmx5aW5nU291cmNlLmNhbmNlbCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGNhbmNlbEFsZ29yaXRobSA9IHJlYXNvbiA9PiB1bmRlcmx5aW5nU291cmNlLmNhbmNlbChyZWFzb24pO1xuICAgIH1cbiAgICBTZXRVcFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCBzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsQWxnb3JpdGhtLCBoaWdoV2F0ZXJNYXJrLCBzaXplQWxnb3JpdGhtKTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLlxuZnVuY3Rpb24gZGVmYXVsdENvbnRyb2xsZXJCcmFuZENoZWNrRXhjZXB0aW9uKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUuJHtuYW1lfSBjYW4gb25seSBiZSB1c2VkIG9uIGEgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlcmApO1xufVxuXG5mdW5jdGlvbiBSZWFkYWJsZVN0cmVhbVRlZShzdHJlYW0sIGNsb25lRm9yQnJhbmNoMikge1xuICAgIGNvbnN0IHJlYWRlciA9IEFjcXVpcmVSZWFkYWJsZVN0cmVhbURlZmF1bHRSZWFkZXIoc3RyZWFtKTtcbiAgICBsZXQgcmVhZGluZyA9IGZhbHNlO1xuICAgIGxldCBjYW5jZWxlZDEgPSBmYWxzZTtcbiAgICBsZXQgY2FuY2VsZWQyID0gZmFsc2U7XG4gICAgbGV0IHJlYXNvbjE7XG4gICAgbGV0IHJlYXNvbjI7XG4gICAgbGV0IGJyYW5jaDE7XG4gICAgbGV0IGJyYW5jaDI7XG4gICAgbGV0IHJlc29sdmVDYW5jZWxQcm9taXNlO1xuICAgIGNvbnN0IGNhbmNlbFByb21pc2UgPSBuZXdQcm9taXNlKHJlc29sdmUgPT4ge1xuICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSA9IHJlc29sdmU7XG4gICAgfSk7XG4gICAgZnVuY3Rpb24gcHVsbEFsZ29yaXRobSgpIHtcbiAgICAgICAgaWYgKHJlYWRpbmcpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgcmVhZGluZyA9IHRydWU7XG4gICAgICAgIGNvbnN0IHJlYWRSZXF1ZXN0ID0ge1xuICAgICAgICAgICAgX2NodW5rU3RlcHM6IHZhbHVlID0+IHtcbiAgICAgICAgICAgICAgICAvLyBUaGlzIG5lZWRzIHRvIGJlIGRlbGF5ZWQgYSBtaWNyb3Rhc2sgYmVjYXVzZSBpdCB0YWtlcyBhdCBsZWFzdCBhIG1pY3JvdGFzayB0byBkZXRlY3QgZXJyb3JzICh1c2luZ1xuICAgICAgICAgICAgICAgIC8vIHJlYWRlci5fY2xvc2VkUHJvbWlzZSBiZWxvdyksIGFuZCB3ZSB3YW50IGVycm9ycyBpbiBzdHJlYW0gdG8gZXJyb3IgYm90aCBicmFuY2hlcyBpbW1lZGlhdGVseS4gV2UgY2Fubm90IGxldFxuICAgICAgICAgICAgICAgIC8vIHN1Y2Nlc3NmdWwgc3luY2hyb25vdXNseS1hdmFpbGFibGUgcmVhZHMgZ2V0IGFoZWFkIG9mIGFzeW5jaHJvbm91c2x5LWF2YWlsYWJsZSBlcnJvcnMuXG4gICAgICAgICAgICAgICAgcXVldWVNaWNyb3Rhc2soKCkgPT4ge1xuICAgICAgICAgICAgICAgICAgICByZWFkaW5nID0gZmFsc2U7XG4gICAgICAgICAgICAgICAgICAgIGNvbnN0IHZhbHVlMSA9IHZhbHVlO1xuICAgICAgICAgICAgICAgICAgICBjb25zdCB2YWx1ZTIgPSB2YWx1ZTtcbiAgICAgICAgICAgICAgICAgICAgLy8gVGhlcmUgaXMgbm8gd2F5IHRvIGFjY2VzcyB0aGUgY2xvbmluZyBjb2RlIHJpZ2h0IG5vdyBpbiB0aGUgcmVmZXJlbmNlIGltcGxlbWVudGF0aW9uLlxuICAgICAgICAgICAgICAgICAgICAvLyBJZiB3ZSBhZGQgb25lIHRoZW4gd2UnbGwgbmVlZCBhbiBpbXBsZW1lbnRhdGlvbiBmb3Igc2VyaWFsaXphYmxlIG9iamVjdHMuXG4gICAgICAgICAgICAgICAgICAgIC8vIGlmICghY2FuY2VsZWQyICYmIGNsb25lRm9yQnJhbmNoMikge1xuICAgICAgICAgICAgICAgICAgICAvLyAgIHZhbHVlMiA9IFN0cnVjdHVyZWREZXNlcmlhbGl6ZShTdHJ1Y3R1cmVkU2VyaWFsaXplKHZhbHVlMikpO1xuICAgICAgICAgICAgICAgICAgICAvLyB9XG4gICAgICAgICAgICAgICAgICAgIGlmICghY2FuY2VsZWQxKSB7XG4gICAgICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRW5xdWV1ZShicmFuY2gxLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIHZhbHVlMSk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgaWYgKCFjYW5jZWxlZDIpIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKGJyYW5jaDIuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgdmFsdWUyKTtcbiAgICAgICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSh1bmRlZmluZWQpO1xuICAgICAgICAgICAgICAgIH0pO1xuICAgICAgICAgICAgfSxcbiAgICAgICAgICAgIF9jbG9zZVN0ZXBzOiAoKSA9PiB7XG4gICAgICAgICAgICAgICAgcmVhZGluZyA9IGZhbHNlO1xuICAgICAgICAgICAgICAgIGlmICghY2FuY2VsZWQxKSB7XG4gICAgICAgICAgICAgICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShicmFuY2gxLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICBpZiAoIWNhbmNlbGVkMikge1xuICAgICAgICAgICAgICAgICAgICBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xvc2UoYnJhbmNoMi5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyKTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9LFxuICAgICAgICAgICAgX2Vycm9yU3RlcHM6ICgpID0+IHtcbiAgICAgICAgICAgICAgICByZWFkaW5nID0gZmFsc2U7XG4gICAgICAgICAgICB9XG4gICAgICAgIH07XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlclJlYWQocmVhZGVyLCByZWFkUmVxdWVzdCk7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNhbmNlbDFBbGdvcml0aG0ocmVhc29uKSB7XG4gICAgICAgIGNhbmNlbGVkMSA9IHRydWU7XG4gICAgICAgIHJlYXNvbjEgPSByZWFzb247XG4gICAgICAgIGlmIChjYW5jZWxlZDIpIHtcbiAgICAgICAgICAgIGNvbnN0IGNvbXBvc2l0ZVJlYXNvbiA9IENyZWF0ZUFycmF5RnJvbUxpc3QoW3JlYXNvbjEsIHJlYXNvbjJdKTtcbiAgICAgICAgICAgIGNvbnN0IGNhbmNlbFJlc3VsdCA9IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgY29tcG9zaXRlUmVhc29uKTtcbiAgICAgICAgICAgIHJlc29sdmVDYW5jZWxQcm9taXNlKGNhbmNlbFJlc3VsdCk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGNhbmNlbFByb21pc2U7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNhbmNlbDJBbGdvcml0aG0ocmVhc29uKSB7XG4gICAgICAgIGNhbmNlbGVkMiA9IHRydWU7XG4gICAgICAgIHJlYXNvbjIgPSByZWFzb247XG4gICAgICAgIGlmIChjYW5jZWxlZDEpIHtcbiAgICAgICAgICAgIGNvbnN0IGNvbXBvc2l0ZVJlYXNvbiA9IENyZWF0ZUFycmF5RnJvbUxpc3QoW3JlYXNvbjEsIHJlYXNvbjJdKTtcbiAgICAgICAgICAgIGNvbnN0IGNhbmNlbFJlc3VsdCA9IFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgY29tcG9zaXRlUmVhc29uKTtcbiAgICAgICAgICAgIHJlc29sdmVDYW5jZWxQcm9taXNlKGNhbmNlbFJlc3VsdCk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIGNhbmNlbFByb21pc2U7XG4gICAgfVxuICAgIGZ1bmN0aW9uIHN0YXJ0QWxnb3JpdGhtKCkge1xuICAgICAgICAvLyBkbyBub3RoaW5nXG4gICAgfVxuICAgIGJyYW5jaDEgPSBDcmVhdGVSZWFkYWJsZVN0cmVhbShzdGFydEFsZ29yaXRobSwgcHVsbEFsZ29yaXRobSwgY2FuY2VsMUFsZ29yaXRobSk7XG4gICAgYnJhbmNoMiA9IENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWwyQWxnb3JpdGhtKTtcbiAgICB1cG9uUmVqZWN0aW9uKHJlYWRlci5fY2xvc2VkUHJvbWlzZSwgKHIpID0+IHtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKGJyYW5jaDEuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlciwgcik7XG4gICAgICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihicmFuY2gyLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIHIpO1xuICAgICAgICByZXNvbHZlQ2FuY2VsUHJvbWlzZSh1bmRlZmluZWQpO1xuICAgIH0pO1xuICAgIHJldHVybiBbYnJhbmNoMSwgYnJhbmNoMl07XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nRGVmYXVsdE9yQnl0ZVNvdXJjZShzb3VyY2UsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KHNvdXJjZSwgY29udGV4dCk7XG4gICAgY29uc3Qgb3JpZ2luYWwgPSBzb3VyY2U7XG4gICAgY29uc3QgYXV0b0FsbG9jYXRlQ2h1bmtTaXplID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLmF1dG9BbGxvY2F0ZUNodW5rU2l6ZTtcbiAgICBjb25zdCBjYW5jZWwgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwuY2FuY2VsO1xuICAgIGNvbnN0IHB1bGwgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwucHVsbDtcbiAgICBjb25zdCBzdGFydCA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5zdGFydDtcbiAgICBjb25zdCB0eXBlID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnR5cGU7XG4gICAgcmV0dXJuIHtcbiAgICAgICAgYXV0b0FsbG9jYXRlQ2h1bmtTaXplOiBhdXRvQWxsb2NhdGVDaHVua1NpemUgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuc2lnbmVkTG9uZ0xvbmdXaXRoRW5mb3JjZVJhbmdlKGF1dG9BbGxvY2F0ZUNodW5rU2l6ZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnYXV0b0FsbG9jYXRlQ2h1bmtTaXplJyB0aGF0YCksXG4gICAgICAgIGNhbmNlbDogY2FuY2VsID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlQ2FuY2VsQ2FsbGJhY2soY2FuY2VsLCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnY2FuY2VsJyB0aGF0YCksXG4gICAgICAgIHB1bGw6IHB1bGwgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFVuZGVybHlpbmdTb3VyY2VQdWxsQ2FsbGJhY2socHVsbCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3B1bGwnIHRoYXRgKSxcbiAgICAgICAgc3RhcnQ6IHN0YXJ0ID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlU3RhcnRDYWxsYmFjayhzdGFydCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3N0YXJ0JyB0aGF0YCksXG4gICAgICAgIHR5cGU6IHR5cGUgPT09IHVuZGVmaW5lZCA/IHVuZGVmaW5lZCA6IGNvbnZlcnRSZWFkYWJsZVN0cmVhbVR5cGUodHlwZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAndHlwZScgdGhhdGApXG4gICAgfTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlQ2FuY2VsQ2FsbGJhY2soZm4sIG9yaWdpbmFsLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RnVuY3Rpb24oZm4sIGNvbnRleHQpO1xuICAgIHJldHVybiAocmVhc29uKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtyZWFzb25dKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlUHVsbENhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNvbnRyb2xsZXIpID0+IHByb21pc2VDYWxsKGZuLCBvcmlnaW5hbCwgW2NvbnRyb2xsZXJdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRVbmRlcmx5aW5nU291cmNlU3RhcnRDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjb250cm9sbGVyKSA9PiByZWZsZWN0Q2FsbChmbiwgb3JpZ2luYWwsIFtjb250cm9sbGVyXSk7XG59XG5mdW5jdGlvbiBjb252ZXJ0UmVhZGFibGVTdHJlYW1UeXBlKHR5cGUsIGNvbnRleHQpIHtcbiAgICB0eXBlID0gYCR7dHlwZX1gO1xuICAgIGlmICh0eXBlICE9PSAnYnl0ZXMnKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoYCR7Y29udGV4dH0gJyR7dHlwZX0nIGlzIG5vdCBhIHZhbGlkIGVudW1lcmF0aW9uIHZhbHVlIGZvciBSZWFkYWJsZVN0cmVhbVR5cGVgKTtcbiAgICB9XG4gICAgcmV0dXJuIHR5cGU7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSZWFkZXJPcHRpb25zKG9wdGlvbnMsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KG9wdGlvbnMsIGNvbnRleHQpO1xuICAgIGNvbnN0IG1vZGUgPSBvcHRpb25zID09PSBudWxsIHx8IG9wdGlvbnMgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9wdGlvbnMubW9kZTtcbiAgICByZXR1cm4ge1xuICAgICAgICBtb2RlOiBtb2RlID09PSB1bmRlZmluZWQgPyB1bmRlZmluZWQgOiBjb252ZXJ0UmVhZGFibGVTdHJlYW1SZWFkZXJNb2RlKG1vZGUsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ21vZGUnIHRoYXRgKVxuICAgIH07XG59XG5mdW5jdGlvbiBjb252ZXJ0UmVhZGFibGVTdHJlYW1SZWFkZXJNb2RlKG1vZGUsIGNvbnRleHQpIHtcbiAgICBtb2RlID0gYCR7bW9kZX1gO1xuICAgIGlmIChtb2RlICE9PSAnYnlvYicpIHtcbiAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcihgJHtjb250ZXh0fSAnJHttb2RlfScgaXMgbm90IGEgdmFsaWQgZW51bWVyYXRpb24gdmFsdWUgZm9yIFJlYWRhYmxlU3RyZWFtUmVhZGVyTW9kZWApO1xuICAgIH1cbiAgICByZXR1cm4gbW9kZTtcbn1cblxuZnVuY3Rpb24gY29udmVydEl0ZXJhdG9yT3B0aW9ucyhvcHRpb25zLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShvcHRpb25zLCBjb250ZXh0KTtcbiAgICBjb25zdCBwcmV2ZW50Q2FuY2VsID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDYW5jZWw7XG4gICAgcmV0dXJuIHsgcHJldmVudENhbmNlbDogQm9vbGVhbihwcmV2ZW50Q2FuY2VsKSB9O1xufVxuXG5mdW5jdGlvbiBjb252ZXJ0UGlwZU9wdGlvbnMob3B0aW9ucywgY29udGV4dCkge1xuICAgIGFzc2VydERpY3Rpb25hcnkob3B0aW9ucywgY29udGV4dCk7XG4gICAgY29uc3QgcHJldmVudEFib3J0ID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRBYm9ydDtcbiAgICBjb25zdCBwcmV2ZW50Q2FuY2VsID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDYW5jZWw7XG4gICAgY29uc3QgcHJldmVudENsb3NlID0gb3B0aW9ucyA9PT0gbnVsbCB8fCBvcHRpb25zID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcHRpb25zLnByZXZlbnRDbG9zZTtcbiAgICBjb25zdCBzaWduYWwgPSBvcHRpb25zID09PSBudWxsIHx8IG9wdGlvbnMgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9wdGlvbnMuc2lnbmFsO1xuICAgIGlmIChzaWduYWwgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICBhc3NlcnRBYm9ydFNpZ25hbChzaWduYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3NpZ25hbCcgdGhhdGApO1xuICAgIH1cbiAgICByZXR1cm4ge1xuICAgICAgICBwcmV2ZW50QWJvcnQ6IEJvb2xlYW4ocHJldmVudEFib3J0KSxcbiAgICAgICAgcHJldmVudENhbmNlbDogQm9vbGVhbihwcmV2ZW50Q2FuY2VsKSxcbiAgICAgICAgcHJldmVudENsb3NlOiBCb29sZWFuKHByZXZlbnRDbG9zZSksXG4gICAgICAgIHNpZ25hbFxuICAgIH07XG59XG5mdW5jdGlvbiBhc3NlcnRBYm9ydFNpZ25hbChzaWduYWwsIGNvbnRleHQpIHtcbiAgICBpZiAoIWlzQWJvcnRTaWduYWwoc2lnbmFsKSkge1xuICAgICAgICB0aHJvdyBuZXcgVHlwZUVycm9yKGAke2NvbnRleHR9IGlzIG5vdCBhbiBBYm9ydFNpZ25hbC5gKTtcbiAgICB9XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpcihwYWlyLCBjb250ZXh0KSB7XG4gICAgYXNzZXJ0RGljdGlvbmFyeShwYWlyLCBjb250ZXh0KTtcbiAgICBjb25zdCByZWFkYWJsZSA9IHBhaXIgPT09IG51bGwgfHwgcGFpciA9PT0gdm9pZCAwID8gdm9pZCAwIDogcGFpci5yZWFkYWJsZTtcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKHJlYWRhYmxlLCAncmVhZGFibGUnLCAnUmVhZGFibGVXcml0YWJsZVBhaXInKTtcbiAgICBhc3NlcnRSZWFkYWJsZVN0cmVhbShyZWFkYWJsZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAncmVhZGFibGUnIHRoYXRgKTtcbiAgICBjb25zdCB3cml0YWJsZSA9IHBhaXIgPT09IG51bGwgfHwgcGFpciA9PT0gdm9pZCAwID8gdm9pZCAwIDogcGFpci53cml0YWJsZTtcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKHdyaXRhYmxlLCAnd3JpdGFibGUnLCAnUmVhZGFibGVXcml0YWJsZVBhaXInKTtcbiAgICBhc3NlcnRXcml0YWJsZVN0cmVhbSh3cml0YWJsZSwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnd3JpdGFibGUnIHRoYXRgKTtcbiAgICByZXR1cm4geyByZWFkYWJsZSwgd3JpdGFibGUgfTtcbn1cblxuLyoqXG4gKiBBIHJlYWRhYmxlIHN0cmVhbSByZXByZXNlbnRzIGEgc291cmNlIG9mIGRhdGEsIGZyb20gd2hpY2ggeW91IGNhbiByZWFkLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgUmVhZGFibGVTdHJlYW0ge1xuICAgIGNvbnN0cnVjdG9yKHJhd1VuZGVybHlpbmdTb3VyY2UgPSB7fSwgcmF3U3RyYXRlZ3kgPSB7fSkge1xuICAgICAgICBpZiAocmF3VW5kZXJseWluZ1NvdXJjZSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICByYXdVbmRlcmx5aW5nU291cmNlID0gbnVsbDtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGFzc2VydE9iamVjdChyYXdVbmRlcmx5aW5nU291cmNlLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3Qgc3RyYXRlZ3kgPSBjb252ZXJ0UXVldWluZ1N0cmF0ZWd5KHJhd1N0cmF0ZWd5LCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBjb25zdCB1bmRlcmx5aW5nU291cmNlID0gY29udmVydFVuZGVybHlpbmdEZWZhdWx0T3JCeXRlU291cmNlKHJhd1VuZGVybHlpbmdTb3VyY2UsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgSW5pdGlhbGl6ZVJlYWRhYmxlU3RyZWFtKHRoaXMpO1xuICAgICAgICBpZiAodW5kZXJseWluZ1NvdXJjZS50eXBlID09PSAnYnl0ZXMnKSB7XG4gICAgICAgICAgICBpZiAoc3RyYXRlZ3kuc2l6ZSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSBzdHJhdGVneSBmb3IgYSBieXRlIHN0cmVhbSBjYW5ub3QgaGF2ZSBhIHNpemUgZnVuY3Rpb24nKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgICAgIGNvbnN0IGhpZ2hXYXRlck1hcmsgPSBFeHRyYWN0SGlnaFdhdGVyTWFyayhzdHJhdGVneSwgMCk7XG4gICAgICAgICAgICBTZXRVcFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXJGcm9tVW5kZXJseWluZ1NvdXJjZSh0aGlzLCB1bmRlcmx5aW5nU291cmNlLCBoaWdoV2F0ZXJNYXJrKTtcbiAgICAgICAgfVxuICAgICAgICBlbHNlIHtcbiAgICAgICAgICAgIGNvbnN0IHNpemVBbGdvcml0aG0gPSBFeHRyYWN0U2l6ZUFsZ29yaXRobShzdHJhdGVneSk7XG4gICAgICAgICAgICBjb25zdCBoaWdoV2F0ZXJNYXJrID0gRXh0cmFjdEhpZ2hXYXRlck1hcmsoc3RyYXRlZ3ksIDEpO1xuICAgICAgICAgICAgU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRnJvbVVuZGVybHlpbmdTb3VyY2UodGhpcywgdW5kZXJseWluZ1NvdXJjZSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgICAgIH1cbiAgICB9XG4gICAgLyoqXG4gICAgICogV2hldGhlciBvciBub3QgdGhlIHJlYWRhYmxlIHN0cmVhbSBpcyBsb2NrZWQgdG8gYSB7QGxpbmsgUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyIHwgcmVhZGVyfS5cbiAgICAgKi9cbiAgICBnZXQgbG9ja2VkKCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnbG9ja2VkJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIElzUmVhZGFibGVTdHJlYW1Mb2NrZWQodGhpcyk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENhbmNlbHMgdGhlIHN0cmVhbSwgc2lnbmFsaW5nIGEgbG9zcyBvZiBpbnRlcmVzdCBpbiB0aGUgc3RyZWFtIGJ5IGEgY29uc3VtZXIuXG4gICAgICpcbiAgICAgKiBUaGUgc3VwcGxpZWQgYHJlYXNvbmAgYXJndW1lbnQgd2lsbCBiZSBnaXZlbiB0byB0aGUgdW5kZXJseWluZyBzb3VyY2UncyB7QGxpbmsgVW5kZXJseWluZ1NvdXJjZS5jYW5jZWwgfCBjYW5jZWwoKX1cbiAgICAgKiBtZXRob2QsIHdoaWNoIG1pZ2h0IG9yIG1pZ2h0IG5vdCB1c2UgaXQuXG4gICAgICovXG4gICAgY2FuY2VsKHJlYXNvbiA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnY2FuY2VsJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCdDYW5ub3QgY2FuY2VsIGEgc3RyZWFtIHRoYXQgYWxyZWFkeSBoYXMgYSByZWFkZXInKSk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIGdldFJlYWRlcihyYXdPcHRpb25zID0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGlmICghSXNSZWFkYWJsZVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdnZXRSZWFkZXInKTtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCBvcHRpb25zID0gY29udmVydFJlYWRlck9wdGlvbnMocmF3T3B0aW9ucywgJ0ZpcnN0IHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAob3B0aW9ucy5tb2RlID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJldHVybiBBY3F1aXJlUmVhZGFibGVTdHJlYW1EZWZhdWx0UmVhZGVyKHRoaXMpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBBY3F1aXJlUmVhZGFibGVTdHJlYW1CWU9CUmVhZGVyKHRoaXMpO1xuICAgIH1cbiAgICBwaXBlVGhyb3VnaChyYXdUcmFuc2Zvcm0sIHJhd09wdGlvbnMgPSB7fSkge1xuICAgICAgICBpZiAoIUlzUmVhZGFibGVTdHJlYW0odGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IHN0cmVhbUJyYW5kQ2hlY2tFeGNlcHRpb24kMSgncGlwZVRocm91Z2gnKTtcbiAgICAgICAgfVxuICAgICAgICBhc3NlcnRSZXF1aXJlZEFyZ3VtZW50KHJhd1RyYW5zZm9ybSwgMSwgJ3BpcGVUaHJvdWdoJyk7XG4gICAgICAgIGNvbnN0IHRyYW5zZm9ybSA9IGNvbnZlcnRSZWFkYWJsZVdyaXRhYmxlUGFpcihyYXdUcmFuc2Zvcm0sICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgY29uc3Qgb3B0aW9ucyA9IGNvbnZlcnRQaXBlT3B0aW9ucyhyYXdPcHRpb25zLCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBpZiAoSXNSZWFkYWJsZVN0cmVhbUxvY2tlZCh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUaHJvdWdoIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFJlYWRhYmxlU3RyZWFtJyk7XG4gICAgICAgIH1cbiAgICAgICAgaWYgKElzV3JpdGFibGVTdHJlYW1Mb2NrZWQodHJhbnNmb3JtLndyaXRhYmxlKSkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUaHJvdWdoIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFdyaXRhYmxlU3RyZWFtJyk7XG4gICAgICAgIH1cbiAgICAgICAgY29uc3QgcHJvbWlzZSA9IFJlYWRhYmxlU3RyZWFtUGlwZVRvKHRoaXMsIHRyYW5zZm9ybS53cml0YWJsZSwgb3B0aW9ucy5wcmV2ZW50Q2xvc2UsIG9wdGlvbnMucHJldmVudEFib3J0LCBvcHRpb25zLnByZXZlbnRDYW5jZWwsIG9wdGlvbnMuc2lnbmFsKTtcbiAgICAgICAgc2V0UHJvbWlzZUlzSGFuZGxlZFRvVHJ1ZShwcm9taXNlKTtcbiAgICAgICAgcmV0dXJuIHRyYW5zZm9ybS5yZWFkYWJsZTtcbiAgICB9XG4gICAgcGlwZVRvKGRlc3RpbmF0aW9uLCByYXdPcHRpb25zID0ge30pIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3BpcGVUbycpKTtcbiAgICAgICAgfVxuICAgICAgICBpZiAoZGVzdGluYXRpb24gPT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgoYFBhcmFtZXRlciAxIGlzIHJlcXVpcmVkIGluICdwaXBlVG8nLmApO1xuICAgICAgICB9XG4gICAgICAgIGlmICghSXNXcml0YWJsZVN0cmVhbShkZXN0aW5hdGlvbikpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKG5ldyBUeXBlRXJyb3IoYFJlYWRhYmxlU3RyZWFtLnByb3RvdHlwZS5waXBlVG8ncyBmaXJzdCBhcmd1bWVudCBtdXN0IGJlIGEgV3JpdGFibGVTdHJlYW1gKSk7XG4gICAgICAgIH1cbiAgICAgICAgbGV0IG9wdGlvbnM7XG4gICAgICAgIHRyeSB7XG4gICAgICAgICAgICBvcHRpb25zID0gY29udmVydFBpcGVPcHRpb25zKHJhd09wdGlvbnMsICdTZWNvbmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIH1cbiAgICAgICAgY2F0Y2ggKGUpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKGUpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtTG9ja2VkKHRoaXMpKSB7XG4gICAgICAgICAgICByZXR1cm4gcHJvbWlzZVJlamVjdGVkV2l0aChuZXcgVHlwZUVycm9yKCdSZWFkYWJsZVN0cmVhbS5wcm90b3R5cGUucGlwZVRvIGNhbm5vdCBiZSB1c2VkIG9uIGEgbG9ja2VkIFJlYWRhYmxlU3RyZWFtJykpO1xuICAgICAgICB9XG4gICAgICAgIGlmIChJc1dyaXRhYmxlU3RyZWFtTG9ja2VkKGRlc3RpbmF0aW9uKSkge1xuICAgICAgICAgICAgcmV0dXJuIHByb21pc2VSZWplY3RlZFdpdGgobmV3IFR5cGVFcnJvcignUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnBpcGVUbyBjYW5ub3QgYmUgdXNlZCBvbiBhIGxvY2tlZCBXcml0YWJsZVN0cmVhbScpKTtcbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gUmVhZGFibGVTdHJlYW1QaXBlVG8odGhpcywgZGVzdGluYXRpb24sIG9wdGlvbnMucHJldmVudENsb3NlLCBvcHRpb25zLnByZXZlbnRBYm9ydCwgb3B0aW9ucy5wcmV2ZW50Q2FuY2VsLCBvcHRpb25zLnNpZ25hbCk7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFRlZXMgdGhpcyByZWFkYWJsZSBzdHJlYW0sIHJldHVybmluZyBhIHR3by1lbGVtZW50IGFycmF5IGNvbnRhaW5pbmcgdGhlIHR3byByZXN1bHRpbmcgYnJhbmNoZXMgYXNcbiAgICAgKiBuZXcge0BsaW5rIFJlYWRhYmxlU3RyZWFtfSBpbnN0YW5jZXMuXG4gICAgICpcbiAgICAgKiBUZWVpbmcgYSBzdHJlYW0gd2lsbCBsb2NrIGl0LCBwcmV2ZW50aW5nIGFueSBvdGhlciBjb25zdW1lciBmcm9tIGFjcXVpcmluZyBhIHJlYWRlci5cbiAgICAgKiBUbyBjYW5jZWwgdGhlIHN0cmVhbSwgY2FuY2VsIGJvdGggb2YgdGhlIHJlc3VsdGluZyBicmFuY2hlczsgYSBjb21wb3NpdGUgY2FuY2VsbGF0aW9uIHJlYXNvbiB3aWxsIHRoZW4gYmVcbiAgICAgKiBwcm9wYWdhdGVkIHRvIHRoZSBzdHJlYW0ncyB1bmRlcmx5aW5nIHNvdXJjZS5cbiAgICAgKlxuICAgICAqIE5vdGUgdGhhdCB0aGUgY2h1bmtzIHNlZW4gaW4gZWFjaCBicmFuY2ggd2lsbCBiZSB0aGUgc2FtZSBvYmplY3QuIElmIHRoZSBjaHVua3MgYXJlIG5vdCBpbW11dGFibGUsXG4gICAgICogdGhpcyBjb3VsZCBhbGxvdyBpbnRlcmZlcmVuY2UgYmV0d2VlbiB0aGUgdHdvIGJyYW5jaGVzLlxuICAgICAqL1xuICAgIHRlZSgpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3RlZScpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IGJyYW5jaGVzID0gUmVhZGFibGVTdHJlYW1UZWUodGhpcyk7XG4gICAgICAgIHJldHVybiBDcmVhdGVBcnJheUZyb21MaXN0KGJyYW5jaGVzKTtcbiAgICB9XG4gICAgdmFsdWVzKHJhd09wdGlvbnMgPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1JlYWRhYmxlU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDEoJ3ZhbHVlcycpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IG9wdGlvbnMgPSBjb252ZXJ0SXRlcmF0b3JPcHRpb25zKHJhd09wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgcmV0dXJuIEFjcXVpcmVSZWFkYWJsZVN0cmVhbUFzeW5jSXRlcmF0b3IodGhpcywgb3B0aW9ucy5wcmV2ZW50Q2FuY2VsKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhSZWFkYWJsZVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICBjYW5jZWw6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGdldFJlYWRlcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgcGlwZVRocm91Z2g6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIHBpcGVUbzogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgdGVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICB2YWx1ZXM6IHsgZW51bWVyYWJsZTogdHJ1ZSB9LFxuICAgIGxvY2tlZDogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1JlYWRhYmxlU3RyZWFtJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLmFzeW5jSXRlcmF0b3IgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFJlYWRhYmxlU3RyZWFtLnByb3RvdHlwZSwgU3ltYm9sUG9seWZpbGwuYXN5bmNJdGVyYXRvciwge1xuICAgICAgICB2YWx1ZTogUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLnZhbHVlcyxcbiAgICAgICAgd3JpdGFibGU6IHRydWUsXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZVxuICAgIH0pO1xufVxuLy8gQWJzdHJhY3Qgb3BlcmF0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuLy8gVGhyb3dzIGlmIGFuZCBvbmx5IGlmIHN0YXJ0QWxnb3JpdGhtIHRocm93cy5cbmZ1bmN0aW9uIENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIGhpZ2hXYXRlck1hcmsgPSAxLCBzaXplQWxnb3JpdGhtID0gKCkgPT4gMSkge1xuICAgIGNvbnN0IHN0cmVhbSA9IE9iamVjdC5jcmVhdGUoUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlKTtcbiAgICBJbml0aWFsaXplUmVhZGFibGVTdHJlYW0oc3RyZWFtKTtcbiAgICBjb25zdCBjb250cm9sbGVyID0gT2JqZWN0LmNyZWF0ZShSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSk7XG4gICAgU2V0VXBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgc3RhcnRBbGdvcml0aG0sIHB1bGxBbGdvcml0aG0sIGNhbmNlbEFsZ29yaXRobSwgaGlnaFdhdGVyTWFyaywgc2l6ZUFsZ29yaXRobSk7XG4gICAgcmV0dXJuIHN0cmVhbTtcbn1cbmZ1bmN0aW9uIEluaXRpYWxpemVSZWFkYWJsZVN0cmVhbShzdHJlYW0pIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ3JlYWRhYmxlJztcbiAgICBzdHJlYW0uX3JlYWRlciA9IHVuZGVmaW5lZDtcbiAgICBzdHJlYW0uX3N0b3JlZEVycm9yID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fZGlzdHVyYmVkID0gZmFsc2U7XG59XG5mdW5jdGlvbiBJc1JlYWRhYmxlU3RyZWFtKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfcmVhZGFibGVTdHJlYW1Db250cm9sbGVyJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbmZ1bmN0aW9uIElzUmVhZGFibGVTdHJlYW1Mb2NrZWQoc3RyZWFtKSB7XG4gICAgaWYgKHN0cmVhbS5fcmVhZGVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cbi8vIFJlYWRhYmxlU3RyZWFtIEFQSSBleHBvc2VkIGZvciBjb250cm9sbGVycy5cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtQ2FuY2VsKHN0cmVhbSwgcmVhc29uKSB7XG4gICAgc3RyZWFtLl9kaXN0dXJiZWQgPSB0cnVlO1xuICAgIGlmIChzdHJlYW0uX3N0YXRlID09PSAnY2xvc2VkJykge1xuICAgICAgICByZXR1cm4gcHJvbWlzZVJlc29sdmVkV2l0aCh1bmRlZmluZWQpO1xuICAgIH1cbiAgICBpZiAoc3RyZWFtLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHN0cmVhbS5fc3RvcmVkRXJyb3IpO1xuICAgIH1cbiAgICBSZWFkYWJsZVN0cmVhbUNsb3NlKHN0cmVhbSk7XG4gICAgY29uc3Qgc291cmNlQ2FuY2VsUHJvbWlzZSA9IHN0cmVhbS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyW0NhbmNlbFN0ZXBzXShyZWFzb24pO1xuICAgIHJldHVybiB0cmFuc2Zvcm1Qcm9taXNlV2l0aChzb3VyY2VDYW5jZWxQcm9taXNlLCBub29wKTtcbn1cbmZ1bmN0aW9uIFJlYWRhYmxlU3RyZWFtQ2xvc2Uoc3RyZWFtKSB7XG4gICAgc3RyZWFtLl9zdGF0ZSA9ICdjbG9zZWQnO1xuICAgIGNvbnN0IHJlYWRlciA9IHN0cmVhbS5fcmVhZGVyO1xuICAgIGlmIChyZWFkZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzLmZvckVhY2gocmVhZFJlcXVlc3QgPT4ge1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2Nsb3NlU3RlcHMoKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzID0gbmV3IFNpbXBsZVF1ZXVlKCk7XG4gICAgfVxuICAgIGRlZmF1bHRSZWFkZXJDbG9zZWRQcm9taXNlUmVzb2x2ZShyZWFkZXIpO1xufVxuZnVuY3Rpb24gUmVhZGFibGVTdHJlYW1FcnJvcihzdHJlYW0sIGUpIHtcbiAgICBzdHJlYW0uX3N0YXRlID0gJ2Vycm9yZWQnO1xuICAgIHN0cmVhbS5fc3RvcmVkRXJyb3IgPSBlO1xuICAgIGNvbnN0IHJlYWRlciA9IHN0cmVhbS5fcmVhZGVyO1xuICAgIGlmIChyZWFkZXIgPT09IHVuZGVmaW5lZCkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGlmIChJc1JlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlcihyZWFkZXIpKSB7XG4gICAgICAgIHJlYWRlci5fcmVhZFJlcXVlc3RzLmZvckVhY2gocmVhZFJlcXVlc3QgPT4ge1xuICAgICAgICAgICAgcmVhZFJlcXVlc3QuX2Vycm9yU3RlcHMoZSk7XG4gICAgICAgIH0pO1xuICAgICAgICByZWFkZXIuX3JlYWRSZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICBlbHNlIHtcbiAgICAgICAgcmVhZGVyLl9yZWFkSW50b1JlcXVlc3RzLmZvckVhY2gocmVhZEludG9SZXF1ZXN0ID0+IHtcbiAgICAgICAgICAgIHJlYWRJbnRvUmVxdWVzdC5fZXJyb3JTdGVwcyhlKTtcbiAgICAgICAgfSk7XG4gICAgICAgIHJlYWRlci5fcmVhZEludG9SZXF1ZXN0cyA9IG5ldyBTaW1wbGVRdWV1ZSgpO1xuICAgIH1cbiAgICBkZWZhdWx0UmVhZGVyQ2xvc2VkUHJvbWlzZVJlamVjdChyZWFkZXIsIGUpO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIFJlYWRhYmxlU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQxKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgUmVhZGFibGVTdHJlYW0ucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFJlYWRhYmxlU3RyZWFtYCk7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KGluaXQsIGNvbnRleHQpIHtcbiAgICBhc3NlcnREaWN0aW9uYXJ5KGluaXQsIGNvbnRleHQpO1xuICAgIGNvbnN0IGhpZ2hXYXRlck1hcmsgPSBpbml0ID09PSBudWxsIHx8IGluaXQgPT09IHZvaWQgMCA/IHZvaWQgMCA6IGluaXQuaGlnaFdhdGVyTWFyaztcbiAgICBhc3NlcnRSZXF1aXJlZEZpZWxkKGhpZ2hXYXRlck1hcmssICdoaWdoV2F0ZXJNYXJrJywgJ1F1ZXVpbmdTdHJhdGVneUluaXQnKTtcbiAgICByZXR1cm4ge1xuICAgICAgICBoaWdoV2F0ZXJNYXJrOiBjb252ZXJ0VW5yZXN0cmljdGVkRG91YmxlKGhpZ2hXYXRlck1hcmspXG4gICAgfTtcbn1cblxuY29uc3QgYnl0ZUxlbmd0aFNpemVGdW5jdGlvbiA9IGZ1bmN0aW9uIHNpemUoY2h1bmspIHtcbiAgICByZXR1cm4gY2h1bmsuYnl0ZUxlbmd0aDtcbn07XG4vKipcbiAqIEEgcXVldWluZyBzdHJhdGVneSB0aGF0IGNvdW50cyB0aGUgbnVtYmVyIG9mIGJ5dGVzIGluIGVhY2ggY2h1bmsuXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5IHtcbiAgICBjb25zdHJ1Y3RvcihvcHRpb25zKSB7XG4gICAgICAgIGFzc2VydFJlcXVpcmVkQXJndW1lbnQob3B0aW9ucywgMSwgJ0J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3knKTtcbiAgICAgICAgb3B0aW9ucyA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KG9wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgdGhpcy5fYnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneUhpZ2hXYXRlck1hcmsgPSBvcHRpb25zLmhpZ2hXYXRlck1hcms7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFJldHVybnMgdGhlIGhpZ2ggd2F0ZXIgbWFyayBwcm92aWRlZCB0byB0aGUgY29uc3RydWN0b3IuXG4gICAgICovXG4gICAgZ2V0IGhpZ2hXYXRlck1hcmsoKSB7XG4gICAgICAgIGlmICghSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbignaGlnaFdhdGVyTWFyaycpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9ieXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyaztcbiAgICB9XG4gICAgLyoqXG4gICAgICogTWVhc3VyZXMgdGhlIHNpemUgb2YgYGNodW5rYCBieSByZXR1cm5pbmcgdGhlIHZhbHVlIG9mIGl0cyBgYnl0ZUxlbmd0aGAgcHJvcGVydHkuXG4gICAgICovXG4gICAgZ2V0IHNpemUoKSB7XG4gICAgICAgIGlmICghSXNCeXRlTGVuZ3RoUXVldWluZ1N0cmF0ZWd5KHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbignc2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBieXRlTGVuZ3RoU2l6ZUZ1bmN0aW9uO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kucHJvdG90eXBlLCB7XG4gICAgaGlnaFdhdGVyTWFyazogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgc2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneScsXG4gICAgICAgIGNvbmZpZ3VyYWJsZTogdHJ1ZVxuICAgIH0pO1xufVxuLy8gSGVscGVyIGZ1bmN0aW9ucyBmb3IgdGhlIEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kuXG5mdW5jdGlvbiBieXRlTGVuZ3RoQnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3kucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIEJ5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lgKTtcbn1cbmZ1bmN0aW9uIElzQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2J5dGVMZW5ndGhRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrJykpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICByZXR1cm4gdHJ1ZTtcbn1cblxuY29uc3QgY291bnRTaXplRnVuY3Rpb24gPSBmdW5jdGlvbiBzaXplKCkge1xuICAgIHJldHVybiAxO1xufTtcbi8qKlxuICogQSBxdWV1aW5nIHN0cmF0ZWd5IHRoYXQgY291bnRzIHRoZSBudW1iZXIgb2YgY2h1bmtzLlxuICpcbiAqIEBwdWJsaWNcbiAqL1xuY2xhc3MgQ291bnRRdWV1aW5nU3RyYXRlZ3kge1xuICAgIGNvbnN0cnVjdG9yKG9wdGlvbnMpIHtcbiAgICAgICAgYXNzZXJ0UmVxdWlyZWRBcmd1bWVudChvcHRpb25zLCAxLCAnQ291bnRRdWV1aW5nU3RyYXRlZ3knKTtcbiAgICAgICAgb3B0aW9ucyA9IGNvbnZlcnRRdWV1aW5nU3RyYXRlZ3lJbml0KG9wdGlvbnMsICdGaXJzdCBwYXJhbWV0ZXInKTtcbiAgICAgICAgdGhpcy5fY291bnRRdWV1aW5nU3RyYXRlZ3lIaWdoV2F0ZXJNYXJrID0gb3B0aW9ucy5oaWdoV2F0ZXJNYXJrO1xuICAgIH1cbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBoaWdoIHdhdGVyIG1hcmsgcHJvdmlkZWQgdG8gdGhlIGNvbnN0cnVjdG9yLlxuICAgICAqL1xuICAgIGdldCBoaWdoV2F0ZXJNYXJrKCkge1xuICAgICAgICBpZiAoIUlzQ291bnRRdWV1aW5nU3RyYXRlZ3kodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbignaGlnaFdhdGVyTWFyaycpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9jb3VudFF1ZXVpbmdTdHJhdGVneUhpZ2hXYXRlck1hcms7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIE1lYXN1cmVzIHRoZSBzaXplIG9mIGBjaHVua2AgYnkgYWx3YXlzIHJldHVybmluZyAxLlxuICAgICAqIFRoaXMgZW5zdXJlcyB0aGF0IHRoZSB0b3RhbCBxdWV1ZSBzaXplIGlzIGEgY291bnQgb2YgdGhlIG51bWJlciBvZiBjaHVua3MgaW4gdGhlIHF1ZXVlLlxuICAgICAqL1xuICAgIGdldCBzaXplKCkge1xuICAgICAgICBpZiAoIUlzQ291bnRRdWV1aW5nU3RyYXRlZ3kodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbignc2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBjb3VudFNpemVGdW5jdGlvbjtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhDb3VudFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIHtcbiAgICBoaWdoV2F0ZXJNYXJrOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBzaXplOiB7IGVudW1lcmFibGU6IHRydWUgfVxufSk7XG5pZiAodHlwZW9mIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnID09PSAnc3ltYm9sJykge1xuICAgIE9iamVjdC5kZWZpbmVQcm9wZXJ0eShDb3VudFF1ZXVpbmdTdHJhdGVneS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnQ291bnRRdWV1aW5nU3RyYXRlZ3knLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICB9KTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBDb3VudFF1ZXVpbmdTdHJhdGVneS5cbmZ1bmN0aW9uIGNvdW50QnJhbmRDaGVja0V4Y2VwdGlvbihuYW1lKSB7XG4gICAgcmV0dXJuIG5ldyBUeXBlRXJyb3IoYENvdW50UXVldWluZ1N0cmF0ZWd5LnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBDb3VudFF1ZXVpbmdTdHJhdGVneWApO1xufVxuZnVuY3Rpb24gSXNDb3VudFF1ZXVpbmdTdHJhdGVneSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX2NvdW50UXVldWluZ1N0cmF0ZWd5SGlnaFdhdGVyTWFyaycpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5cbmZ1bmN0aW9uIGNvbnZlcnRUcmFuc2Zvcm1lcihvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydERpY3Rpb25hcnkob3JpZ2luYWwsIGNvbnRleHQpO1xuICAgIGNvbnN0IGZsdXNoID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLmZsdXNoO1xuICAgIGNvbnN0IHJlYWRhYmxlVHlwZSA9IG9yaWdpbmFsID09PSBudWxsIHx8IG9yaWdpbmFsID09PSB2b2lkIDAgPyB2b2lkIDAgOiBvcmlnaW5hbC5yZWFkYWJsZVR5cGU7XG4gICAgY29uc3Qgc3RhcnQgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwuc3RhcnQ7XG4gICAgY29uc3QgdHJhbnNmb3JtID0gb3JpZ2luYWwgPT09IG51bGwgfHwgb3JpZ2luYWwgPT09IHZvaWQgMCA/IHZvaWQgMCA6IG9yaWdpbmFsLnRyYW5zZm9ybTtcbiAgICBjb25zdCB3cml0YWJsZVR5cGUgPSBvcmlnaW5hbCA9PT0gbnVsbCB8fCBvcmlnaW5hbCA9PT0gdm9pZCAwID8gdm9pZCAwIDogb3JpZ2luYWwud3JpdGFibGVUeXBlO1xuICAgIHJldHVybiB7XG4gICAgICAgIGZsdXNoOiBmbHVzaCA9PT0gdW5kZWZpbmVkID9cbiAgICAgICAgICAgIHVuZGVmaW5lZCA6XG4gICAgICAgICAgICBjb252ZXJ0VHJhbnNmb3JtZXJGbHVzaENhbGxiYWNrKGZsdXNoLCBvcmlnaW5hbCwgYCR7Y29udGV4dH0gaGFzIG1lbWJlciAnZmx1c2gnIHRoYXRgKSxcbiAgICAgICAgcmVhZGFibGVUeXBlLFxuICAgICAgICBzdGFydDogc3RhcnQgPT09IHVuZGVmaW5lZCA/XG4gICAgICAgICAgICB1bmRlZmluZWQgOlxuICAgICAgICAgICAgY29udmVydFRyYW5zZm9ybWVyU3RhcnRDYWxsYmFjayhzdGFydCwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3N0YXJ0JyB0aGF0YCksXG4gICAgICAgIHRyYW5zZm9ybTogdHJhbnNmb3JtID09PSB1bmRlZmluZWQgP1xuICAgICAgICAgICAgdW5kZWZpbmVkIDpcbiAgICAgICAgICAgIGNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrKHRyYW5zZm9ybSwgb3JpZ2luYWwsIGAke2NvbnRleHR9IGhhcyBtZW1iZXIgJ3RyYW5zZm9ybScgdGhhdGApLFxuICAgICAgICB3cml0YWJsZVR5cGVcbiAgICB9O1xufVxuZnVuY3Rpb24gY29udmVydFRyYW5zZm9ybWVyRmx1c2hDYWxsYmFjayhmbiwgb3JpZ2luYWwsIGNvbnRleHQpIHtcbiAgICBhc3NlcnRGdW5jdGlvbihmbiwgY29udGV4dCk7XG4gICAgcmV0dXJuIChjb250cm9sbGVyKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtjb250cm9sbGVyXSk7XG59XG5mdW5jdGlvbiBjb252ZXJ0VHJhbnNmb3JtZXJTdGFydENhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNvbnRyb2xsZXIpID0+IHJlZmxlY3RDYWxsKGZuLCBvcmlnaW5hbCwgW2NvbnRyb2xsZXJdKTtcbn1cbmZ1bmN0aW9uIGNvbnZlcnRUcmFuc2Zvcm1lclRyYW5zZm9ybUNhbGxiYWNrKGZuLCBvcmlnaW5hbCwgY29udGV4dCkge1xuICAgIGFzc2VydEZ1bmN0aW9uKGZuLCBjb250ZXh0KTtcbiAgICByZXR1cm4gKGNodW5rLCBjb250cm9sbGVyKSA9PiBwcm9taXNlQ2FsbChmbiwgb3JpZ2luYWwsIFtjaHVuaywgY29udHJvbGxlcl0pO1xufVxuXG4vLyBDbGFzcyBUcmFuc2Zvcm1TdHJlYW1cbi8qKlxuICogQSB0cmFuc2Zvcm0gc3RyZWFtIGNvbnNpc3RzIG9mIGEgcGFpciBvZiBzdHJlYW1zOiBhIHtAbGluayBXcml0YWJsZVN0cmVhbSB8IHdyaXRhYmxlIHN0cmVhbX0sXG4gKiBrbm93biBhcyBpdHMgd3JpdGFibGUgc2lkZSwgYW5kIGEge0BsaW5rIFJlYWRhYmxlU3RyZWFtIHwgcmVhZGFibGUgc3RyZWFtfSwga25vd24gYXMgaXRzIHJlYWRhYmxlIHNpZGUuXG4gKiBJbiBhIG1hbm5lciBzcGVjaWZpYyB0byB0aGUgdHJhbnNmb3JtIHN0cmVhbSBpbiBxdWVzdGlvbiwgd3JpdGVzIHRvIHRoZSB3cml0YWJsZSBzaWRlIHJlc3VsdCBpbiBuZXcgZGF0YSBiZWluZ1xuICogbWFkZSBhdmFpbGFibGUgZm9yIHJlYWRpbmcgZnJvbSB0aGUgcmVhZGFibGUgc2lkZS5cbiAqXG4gKiBAcHVibGljXG4gKi9cbmNsYXNzIFRyYW5zZm9ybVN0cmVhbSB7XG4gICAgY29uc3RydWN0b3IocmF3VHJhbnNmb3JtZXIgPSB7fSwgcmF3V3JpdGFibGVTdHJhdGVneSA9IHt9LCByYXdSZWFkYWJsZVN0cmF0ZWd5ID0ge30pIHtcbiAgICAgICAgaWYgKHJhd1RyYW5zZm9ybWVyID09PSB1bmRlZmluZWQpIHtcbiAgICAgICAgICAgIHJhd1RyYW5zZm9ybWVyID0gbnVsbDtcbiAgICAgICAgfVxuICAgICAgICBjb25zdCB3cml0YWJsZVN0cmF0ZWd5ID0gY29udmVydFF1ZXVpbmdTdHJhdGVneShyYXdXcml0YWJsZVN0cmF0ZWd5LCAnU2Vjb25kIHBhcmFtZXRlcicpO1xuICAgICAgICBjb25zdCByZWFkYWJsZVN0cmF0ZWd5ID0gY29udmVydFF1ZXVpbmdTdHJhdGVneShyYXdSZWFkYWJsZVN0cmF0ZWd5LCAnVGhpcmQgcGFyYW1ldGVyJyk7XG4gICAgICAgIGNvbnN0IHRyYW5zZm9ybWVyID0gY29udmVydFRyYW5zZm9ybWVyKHJhd1RyYW5zZm9ybWVyLCAnRmlyc3QgcGFyYW1ldGVyJyk7XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci5yZWFkYWJsZVR5cGUgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ludmFsaWQgcmVhZGFibGVUeXBlIHNwZWNpZmllZCcpO1xuICAgICAgICB9XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci53cml0YWJsZVR5cGUgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICAgICAgdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ0ludmFsaWQgd3JpdGFibGVUeXBlIHNwZWNpZmllZCcpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHJlYWRhYmxlSGlnaFdhdGVyTWFyayA9IEV4dHJhY3RIaWdoV2F0ZXJNYXJrKHJlYWRhYmxlU3RyYXRlZ3ksIDApO1xuICAgICAgICBjb25zdCByZWFkYWJsZVNpemVBbGdvcml0aG0gPSBFeHRyYWN0U2l6ZUFsZ29yaXRobShyZWFkYWJsZVN0cmF0ZWd5KTtcbiAgICAgICAgY29uc3Qgd3JpdGFibGVIaWdoV2F0ZXJNYXJrID0gRXh0cmFjdEhpZ2hXYXRlck1hcmsod3JpdGFibGVTdHJhdGVneSwgMSk7XG4gICAgICAgIGNvbnN0IHdyaXRhYmxlU2l6ZUFsZ29yaXRobSA9IEV4dHJhY3RTaXplQWxnb3JpdGhtKHdyaXRhYmxlU3RyYXRlZ3kpO1xuICAgICAgICBsZXQgc3RhcnRQcm9taXNlX3Jlc29sdmU7XG4gICAgICAgIGNvbnN0IHN0YXJ0UHJvbWlzZSA9IG5ld1Byb21pc2UocmVzb2x2ZSA9PiB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSA9IHJlc29sdmU7XG4gICAgICAgIH0pO1xuICAgICAgICBJbml0aWFsaXplVHJhbnNmb3JtU3RyZWFtKHRoaXMsIHN0YXJ0UHJvbWlzZSwgd3JpdGFibGVIaWdoV2F0ZXJNYXJrLCB3cml0YWJsZVNpemVBbGdvcml0aG0sIHJlYWRhYmxlSGlnaFdhdGVyTWFyaywgcmVhZGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICAgICAgU2V0VXBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckZyb21UcmFuc2Zvcm1lcih0aGlzLCB0cmFuc2Zvcm1lcik7XG4gICAgICAgIGlmICh0cmFuc2Zvcm1lci5zdGFydCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSh0cmFuc2Zvcm1lci5zdGFydCh0aGlzLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyKSk7XG4gICAgICAgIH1cbiAgICAgICAgZWxzZSB7XG4gICAgICAgICAgICBzdGFydFByb21pc2VfcmVzb2x2ZSh1bmRlZmluZWQpO1xuICAgICAgICB9XG4gICAgfVxuICAgIC8qKlxuICAgICAqIFRoZSByZWFkYWJsZSBzaWRlIG9mIHRoZSB0cmFuc2Zvcm0gc3RyZWFtLlxuICAgICAqL1xuICAgIGdldCByZWFkYWJsZSgpIHtcbiAgICAgICAgaWYgKCFJc1RyYW5zZm9ybVN0cmVhbSh0aGlzKSkge1xuICAgICAgICAgICAgdGhyb3cgc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQyKCdyZWFkYWJsZScpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiB0aGlzLl9yZWFkYWJsZTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogVGhlIHdyaXRhYmxlIHNpZGUgb2YgdGhlIHRyYW5zZm9ybSBzdHJlYW0uXG4gICAgICovXG4gICAgZ2V0IHdyaXRhYmxlKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBzdHJlYW1CcmFuZENoZWNrRXhjZXB0aW9uJDIoJ3dyaXRhYmxlJyk7XG4gICAgICAgIH1cbiAgICAgICAgcmV0dXJuIHRoaXMuX3dyaXRhYmxlO1xuICAgIH1cbn1cbk9iamVjdC5kZWZpbmVQcm9wZXJ0aWVzKFRyYW5zZm9ybVN0cmVhbS5wcm90b3R5cGUsIHtcbiAgICByZWFkYWJsZTogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgd3JpdGFibGU6IHsgZW51bWVyYWJsZTogdHJ1ZSB9XG59KTtcbmlmICh0eXBlb2YgU3ltYm9sUG9seWZpbGwudG9TdHJpbmdUYWcgPT09ICdzeW1ib2wnKSB7XG4gICAgT2JqZWN0LmRlZmluZVByb3BlcnR5KFRyYW5zZm9ybVN0cmVhbS5wcm90b3R5cGUsIFN5bWJvbFBvbHlmaWxsLnRvU3RyaW5nVGFnLCB7XG4gICAgICAgIHZhbHVlOiAnVHJhbnNmb3JtU3RyZWFtJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG5mdW5jdGlvbiBJbml0aWFsaXplVHJhbnNmb3JtU3RyZWFtKHN0cmVhbSwgc3RhcnRQcm9taXNlLCB3cml0YWJsZUhpZ2hXYXRlck1hcmssIHdyaXRhYmxlU2l6ZUFsZ29yaXRobSwgcmVhZGFibGVIaWdoV2F0ZXJNYXJrLCByZWFkYWJsZVNpemVBbGdvcml0aG0pIHtcbiAgICBmdW5jdGlvbiBzdGFydEFsZ29yaXRobSgpIHtcbiAgICAgICAgcmV0dXJuIHN0YXJ0UHJvbWlzZTtcbiAgICB9XG4gICAgZnVuY3Rpb24gd3JpdGVBbGdvcml0aG0oY2h1bmspIHtcbiAgICAgICAgcmV0dXJuIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rV3JpdGVBbGdvcml0aG0oc3RyZWFtLCBjaHVuayk7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGFib3J0QWxnb3JpdGhtKHJlYXNvbikge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtBYm9ydEFsZ29yaXRobShzdHJlYW0sIHJlYXNvbik7XG4gICAgfVxuICAgIGZ1bmN0aW9uIGNsb3NlQWxnb3JpdGhtKCkge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtDbG9zZUFsZ29yaXRobShzdHJlYW0pO1xuICAgIH1cbiAgICBzdHJlYW0uX3dyaXRhYmxlID0gQ3JlYXRlV3JpdGFibGVTdHJlYW0oc3RhcnRBbGdvcml0aG0sIHdyaXRlQWxnb3JpdGhtLCBjbG9zZUFsZ29yaXRobSwgYWJvcnRBbGdvcml0aG0sIHdyaXRhYmxlSGlnaFdhdGVyTWFyaywgd3JpdGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICBmdW5jdGlvbiBwdWxsQWxnb3JpdGhtKCkge1xuICAgICAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNvdXJjZVB1bGxBbGdvcml0aG0oc3RyZWFtKTtcbiAgICB9XG4gICAgZnVuY3Rpb24gY2FuY2VsQWxnb3JpdGhtKHJlYXNvbikge1xuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcldyaXRhYmxlQW5kVW5ibG9ja1dyaXRlKHN0cmVhbSwgcmVhc29uKTtcbiAgICAgICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbiAgICB9XG4gICAgc3RyZWFtLl9yZWFkYWJsZSA9IENyZWF0ZVJlYWRhYmxlU3RyZWFtKHN0YXJ0QWxnb3JpdGhtLCBwdWxsQWxnb3JpdGhtLCBjYW5jZWxBbGdvcml0aG0sIHJlYWRhYmxlSGlnaFdhdGVyTWFyaywgcmVhZGFibGVTaXplQWxnb3JpdGhtKTtcbiAgICAvLyBUaGUgW1tiYWNrcHJlc3N1cmVdXSBzbG90IGlzIHNldCB0byB1bmRlZmluZWQgc28gdGhhdCBpdCBjYW4gYmUgaW5pdGlhbGlzZWQgYnkgVHJhbnNmb3JtU3RyZWFtU2V0QmFja3ByZXNzdXJlLlxuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlID0gdW5kZWZpbmVkO1xuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSA9IHVuZGVmaW5lZDtcbiAgICBzdHJlYW0uX2JhY2twcmVzc3VyZUNoYW5nZVByb21pc2VfcmVzb2x2ZSA9IHVuZGVmaW5lZDtcbiAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCB0cnVlKTtcbiAgICBzdHJlYW0uX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXIgPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBJc1RyYW5zZm9ybVN0cmVhbSh4KSB7XG4gICAgaWYgKCF0eXBlSXNPYmplY3QoeCkpIHtcbiAgICAgICAgcmV0dXJuIGZhbHNlO1xuICAgIH1cbiAgICBpZiAoIU9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbCh4LCAnX3RyYW5zZm9ybVN0cmVhbUNvbnRyb2xsZXInKSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIHJldHVybiB0cnVlO1xufVxuLy8gVGhpcyBpcyBhIG5vLW9wIGlmIGJvdGggc2lkZXMgYXJlIGFscmVhZHkgZXJyb3JlZC5cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbUVycm9yKHN0cmVhbSwgZSkge1xuICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFcnJvcihzdHJlYW0uX3JlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIsIGUpO1xuICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUoc3RyZWFtLCBlKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbUVycm9yV3JpdGFibGVBbmRVbmJsb2NrV3JpdGUoc3RyZWFtLCBlKSB7XG4gICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyKTtcbiAgICBXcml0YWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3JJZk5lZWRlZChzdHJlYW0uX3dyaXRhYmxlLl93cml0YWJsZVN0cmVhbUNvbnRyb2xsZXIsIGUpO1xuICAgIGlmIChzdHJlYW0uX2JhY2twcmVzc3VyZSkge1xuICAgICAgICAvLyBQcmV0ZW5kIHRoYXQgcHVsbCgpIHdhcyBjYWxsZWQgdG8gcGVybWl0IGFueSBwZW5kaW5nIHdyaXRlKCkgY2FsbHMgdG8gY29tcGxldGUuIFRyYW5zZm9ybVN0cmVhbVNldEJhY2twcmVzc3VyZSgpXG4gICAgICAgIC8vIGNhbm5vdCBiZSBjYWxsZWQgZnJvbSBlbnF1ZXVlKCkgb3IgcHVsbCgpIG9uY2UgdGhlIFJlYWRhYmxlU3RyZWFtIGlzIGVycm9yZWQsIHNvIHRoaXMgd2lsbCB3aWxsIGJlIHRoZSBmaW5hbCB0aW1lXG4gICAgICAgIC8vIF9iYWNrcHJlc3N1cmUgaXMgc2V0LlxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCBmYWxzZSk7XG4gICAgfVxufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtU2V0QmFja3ByZXNzdXJlKHN0cmVhbSwgYmFja3ByZXNzdXJlKSB7XG4gICAgLy8gUGFzc2VzIGFsc28gd2hlbiBjYWxsZWQgZHVyaW5nIGNvbnN0cnVjdGlvbi5cbiAgICBpZiAoc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlICE9PSB1bmRlZmluZWQpIHtcbiAgICAgICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlX3Jlc29sdmUoKTtcbiAgICB9XG4gICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlID0gbmV3UHJvbWlzZShyZXNvbHZlID0+IHtcbiAgICAgICAgc3RyZWFtLl9iYWNrcHJlc3N1cmVDaGFuZ2VQcm9taXNlX3Jlc29sdmUgPSByZXNvbHZlO1xuICAgIH0pO1xuICAgIHN0cmVhbS5fYmFja3ByZXNzdXJlID0gYmFja3ByZXNzdXJlO1xufVxuLy8gQ2xhc3MgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJcbi8qKlxuICogQWxsb3dzIGNvbnRyb2wgb2YgdGhlIHtAbGluayBSZWFkYWJsZVN0cmVhbX0gYW5kIHtAbGluayBXcml0YWJsZVN0cmVhbX0gb2YgdGhlIGFzc29jaWF0ZWQge0BsaW5rIFRyYW5zZm9ybVN0cmVhbX0uXG4gKlxuICogQHB1YmxpY1xuICovXG5jbGFzcyBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciB7XG4gICAgY29uc3RydWN0b3IoKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ0lsbGVnYWwgY29uc3RydWN0b3InKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogUmV0dXJucyB0aGUgZGVzaXJlZCBzaXplIHRvIGZpbGwgdGhlIHJlYWRhYmxlIHNpZGXigJlzIGludGVybmFsIHF1ZXVlLiBJdCBjYW4gYmUgbmVnYXRpdmUsIGlmIHRoZSBxdWV1ZSBpcyBvdmVyLWZ1bGwuXG4gICAgICovXG4gICAgZ2V0IGRlc2lyZWRTaXplKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdkZXNpcmVkU2l6ZScpO1xuICAgICAgICB9XG4gICAgICAgIGNvbnN0IHJlYWRhYmxlQ29udHJvbGxlciA9IHRoaXMuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0uX3JlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXI7XG4gICAgICAgIHJldHVybiBSZWFkYWJsZVN0cmVhbURlZmF1bHRDb250cm9sbGVyR2V0RGVzaXJlZFNpemUocmVhZGFibGVDb250cm9sbGVyKTtcbiAgICB9XG4gICAgZW5xdWV1ZShjaHVuayA9IHVuZGVmaW5lZCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCdlbnF1ZXVlJyk7XG4gICAgICAgIH1cbiAgICAgICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKHRoaXMsIGNodW5rKTtcbiAgICB9XG4gICAgLyoqXG4gICAgICogRXJyb3JzIGJvdGggdGhlIHJlYWRhYmxlIHNpZGUgYW5kIHRoZSB3cml0YWJsZSBzaWRlIG9mIHRoZSBjb250cm9sbGVkIHRyYW5zZm9ybSBzdHJlYW0sIG1ha2luZyBhbGwgZnV0dXJlXG4gICAgICogaW50ZXJhY3Rpb25zIHdpdGggaXQgZmFpbCB3aXRoIHRoZSBnaXZlbiBlcnJvciBgZWAuIEFueSBjaHVua3MgcXVldWVkIGZvciB0cmFuc2Zvcm1hdGlvbiB3aWxsIGJlIGRpc2NhcmRlZC5cbiAgICAgKi9cbiAgICBlcnJvcihyZWFzb24gPSB1bmRlZmluZWQpIHtcbiAgICAgICAgaWYgKCFJc1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHRoaXMpKSB7XG4gICAgICAgICAgICB0aHJvdyBkZWZhdWx0Q29udHJvbGxlckJyYW5kQ2hlY2tFeGNlcHRpb24kMSgnZXJyb3InKTtcbiAgICAgICAgfVxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckVycm9yKHRoaXMsIHJlYXNvbik7XG4gICAgfVxuICAgIC8qKlxuICAgICAqIENsb3NlcyB0aGUgcmVhZGFibGUgc2lkZSBhbmQgZXJyb3JzIHRoZSB3cml0YWJsZSBzaWRlIG9mIHRoZSBjb250cm9sbGVkIHRyYW5zZm9ybSBzdHJlYW0uIFRoaXMgaXMgdXNlZnVsIHdoZW4gdGhlXG4gICAgICogdHJhbnNmb3JtZXIgb25seSBuZWVkcyB0byBjb25zdW1lIGEgcG9ydGlvbiBvZiB0aGUgY2h1bmtzIHdyaXR0ZW4gdG8gdGhlIHdyaXRhYmxlIHNpZGUuXG4gICAgICovXG4gICAgdGVybWluYXRlKCkge1xuICAgICAgICBpZiAoIUlzVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIodGhpcykpIHtcbiAgICAgICAgICAgIHRocm93IGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKCd0ZXJtaW5hdGUnKTtcbiAgICAgICAgfVxuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlclRlcm1pbmF0ZSh0aGlzKTtcbiAgICB9XG59XG5PYmplY3QuZGVmaW5lUHJvcGVydGllcyhUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlci5wcm90b3R5cGUsIHtcbiAgICBlbnF1ZXVlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBlcnJvcjogeyBlbnVtZXJhYmxlOiB0cnVlIH0sXG4gICAgdGVybWluYXRlOiB7IGVudW1lcmFibGU6IHRydWUgfSxcbiAgICBkZXNpcmVkU2l6ZTogeyBlbnVtZXJhYmxlOiB0cnVlIH1cbn0pO1xuaWYgKHR5cGVvZiBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZyA9PT0gJ3N5bWJvbCcpIHtcbiAgICBPYmplY3QuZGVmaW5lUHJvcGVydHkoVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlLCBTeW1ib2xQb2x5ZmlsbC50b1N0cmluZ1RhZywge1xuICAgICAgICB2YWx1ZTogJ1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyJyxcbiAgICAgICAgY29uZmlndXJhYmxlOiB0cnVlXG4gICAgfSk7XG59XG4vLyBUcmFuc2Zvcm0gU3RyZWFtIERlZmF1bHQgQ29udHJvbGxlciBBYnN0cmFjdCBPcGVyYXRpb25zXG5mdW5jdGlvbiBJc1RyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHgpIHtcbiAgICBpZiAoIXR5cGVJc09iamVjdCh4KSkge1xuICAgICAgICByZXR1cm4gZmFsc2U7XG4gICAgfVxuICAgIGlmICghT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKHgsICdfY29udHJvbGxlZFRyYW5zZm9ybVN0cmVhbScpKSB7XG4gICAgICAgIHJldHVybiBmYWxzZTtcbiAgICB9XG4gICAgcmV0dXJuIHRydWU7XG59XG5mdW5jdGlvbiBTZXRVcFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyKHN0cmVhbSwgY29udHJvbGxlciwgdHJhbnNmb3JtQWxnb3JpdGhtLCBmbHVzaEFsZ29yaXRobSkge1xuICAgIGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0gPSBzdHJlYW07XG4gICAgc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyID0gY29udHJvbGxlcjtcbiAgICBjb250cm9sbGVyLl90cmFuc2Zvcm1BbGdvcml0aG0gPSB0cmFuc2Zvcm1BbGdvcml0aG07XG4gICAgY29udHJvbGxlci5fZmx1c2hBbGdvcml0aG0gPSBmbHVzaEFsZ29yaXRobTtcbn1cbmZ1bmN0aW9uIFNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJGcm9tVHJhbnNmb3JtZXIoc3RyZWFtLCB0cmFuc2Zvcm1lcikge1xuICAgIGNvbnN0IGNvbnRyb2xsZXIgPSBPYmplY3QuY3JlYXRlKFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyLnByb3RvdHlwZSk7XG4gICAgbGV0IHRyYW5zZm9ybUFsZ29yaXRobSA9IChjaHVuaykgPT4ge1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKGNvbnRyb2xsZXIsIGNodW5rKTtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgICAgIH1cbiAgICAgICAgY2F0Y2ggKHRyYW5zZm9ybVJlc3VsdEUpIHtcbiAgICAgICAgICAgIHJldHVybiBwcm9taXNlUmVqZWN0ZWRXaXRoKHRyYW5zZm9ybVJlc3VsdEUpO1xuICAgICAgICB9XG4gICAgfTtcbiAgICBsZXQgZmx1c2hBbGdvcml0aG0gPSAoKSA9PiBwcm9taXNlUmVzb2x2ZWRXaXRoKHVuZGVmaW5lZCk7XG4gICAgaWYgKHRyYW5zZm9ybWVyLnRyYW5zZm9ybSAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIHRyYW5zZm9ybUFsZ29yaXRobSA9IGNodW5rID0+IHRyYW5zZm9ybWVyLnRyYW5zZm9ybShjaHVuaywgY29udHJvbGxlcik7XG4gICAgfVxuICAgIGlmICh0cmFuc2Zvcm1lci5mbHVzaCAhPT0gdW5kZWZpbmVkKSB7XG4gICAgICAgIGZsdXNoQWxnb3JpdGhtID0gKCkgPT4gdHJhbnNmb3JtZXIuZmx1c2goY29udHJvbGxlcik7XG4gICAgfVxuICAgIFNldFVwVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIoc3RyZWFtLCBjb250cm9sbGVyLCB0cmFuc2Zvcm1BbGdvcml0aG0sIGZsdXNoQWxnb3JpdGhtKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyQ2xlYXJBbGdvcml0aG1zKGNvbnRyb2xsZXIpIHtcbiAgICBjb250cm9sbGVyLl90cmFuc2Zvcm1BbGdvcml0aG0gPSB1bmRlZmluZWQ7XG4gICAgY29udHJvbGxlci5fZmx1c2hBbGdvcml0aG0gPSB1bmRlZmluZWQ7XG59XG5mdW5jdGlvbiBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUoY29udHJvbGxlciwgY2h1bmspIHtcbiAgICBjb25zdCBzdHJlYW0gPSBjb250cm9sbGVyLl9jb250cm9sbGVkVHJhbnNmb3JtU3RyZWFtO1xuICAgIGNvbnN0IHJlYWRhYmxlQ29udHJvbGxlciA9IHN0cmVhbS5fcmVhZGFibGUuX3JlYWRhYmxlU3RyZWFtQ29udHJvbGxlcjtcbiAgICBpZiAoIVJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDYW5DbG9zZU9yRW5xdWV1ZShyZWFkYWJsZUNvbnRyb2xsZXIpKSB7XG4gICAgICAgIHRocm93IG5ldyBUeXBlRXJyb3IoJ1JlYWRhYmxlIHNpZGUgaXMgbm90IGluIGEgc3RhdGUgdGhhdCBwZXJtaXRzIGVucXVldWUnKTtcbiAgICB9XG4gICAgLy8gV2UgdGhyb3R0bGUgdHJhbnNmb3JtIGludm9jYXRpb25zIGJhc2VkIG9uIHRoZSBiYWNrcHJlc3N1cmUgb2YgdGhlIFJlYWRhYmxlU3RyZWFtLCBidXQgd2Ugc3RpbGxcbiAgICAvLyBhY2NlcHQgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJFbnF1ZXVlKCkgY2FsbHMuXG4gICAgdHJ5IHtcbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckVucXVldWUocmVhZGFibGVDb250cm9sbGVyLCBjaHVuayk7XG4gICAgfVxuICAgIGNhdGNoIChlKSB7XG4gICAgICAgIC8vIFRoaXMgaGFwcGVucyB3aGVuIHJlYWRhYmxlU3RyYXRlZ3kuc2l6ZSgpIHRocm93cy5cbiAgICAgICAgVHJhbnNmb3JtU3RyZWFtRXJyb3JXcml0YWJsZUFuZFVuYmxvY2tXcml0ZShzdHJlYW0sIGUpO1xuICAgICAgICB0aHJvdyBzdHJlYW0uX3JlYWRhYmxlLl9zdG9yZWRFcnJvcjtcbiAgICB9XG4gICAgY29uc3QgYmFja3ByZXNzdXJlID0gUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckhhc0JhY2twcmVzc3VyZShyZWFkYWJsZUNvbnRyb2xsZXIpO1xuICAgIGlmIChiYWNrcHJlc3N1cmUgIT09IHN0cmVhbS5fYmFja3ByZXNzdXJlKSB7XG4gICAgICAgIFRyYW5zZm9ybVN0cmVhbVNldEJhY2twcmVzc3VyZShzdHJlYW0sIHRydWUpO1xuICAgIH1cbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyRXJyb3IoY29udHJvbGxlciwgZSkge1xuICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0sIGUpO1xufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQZXJmb3JtVHJhbnNmb3JtKGNvbnRyb2xsZXIsIGNodW5rKSB7XG4gICAgY29uc3QgdHJhbnNmb3JtUHJvbWlzZSA9IGNvbnRyb2xsZXIuX3RyYW5zZm9ybUFsZ29yaXRobShjaHVuayk7XG4gICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKHRyYW5zZm9ybVByb21pc2UsIHVuZGVmaW5lZCwgciA9PiB7XG4gICAgICAgIFRyYW5zZm9ybVN0cmVhbUVycm9yKGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW0sIHIpO1xuICAgICAgICB0aHJvdyByO1xuICAgIH0pO1xufVxuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJUZXJtaW5hdGUoY29udHJvbGxlcikge1xuICAgIGNvbnN0IHN0cmVhbSA9IGNvbnRyb2xsZXIuX2NvbnRyb2xsZWRUcmFuc2Zvcm1TdHJlYW07XG4gICAgY29uc3QgcmVhZGFibGVDb250cm9sbGVyID0gc3RyZWFtLl9yZWFkYWJsZS5fcmVhZGFibGVTdHJlYW1Db250cm9sbGVyO1xuICAgIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbG9zZShyZWFkYWJsZUNvbnRyb2xsZXIpO1xuICAgIGNvbnN0IGVycm9yID0gbmV3IFR5cGVFcnJvcignVHJhbnNmb3JtU3RyZWFtIHRlcm1pbmF0ZWQnKTtcbiAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcldyaXRhYmxlQW5kVW5ibG9ja1dyaXRlKHN0cmVhbSwgZXJyb3IpO1xufVxuLy8gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmsgQWxnb3JpdGhtc1xuZnVuY3Rpb24gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdFNpbmtXcml0ZUFsZ29yaXRobShzdHJlYW0sIGNodW5rKSB7XG4gICAgY29uc3QgY29udHJvbGxlciA9IHN0cmVhbS5fdHJhbnNmb3JtU3RyZWFtQ29udHJvbGxlcjtcbiAgICBpZiAoc3RyZWFtLl9iYWNrcHJlc3N1cmUpIHtcbiAgICAgICAgY29uc3QgYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZSA9IHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZTtcbiAgICAgICAgcmV0dXJuIHRyYW5zZm9ybVByb21pc2VXaXRoKGJhY2twcmVzc3VyZUNoYW5nZVByb21pc2UsICgpID0+IHtcbiAgICAgICAgICAgIGNvbnN0IHdyaXRhYmxlID0gc3RyZWFtLl93cml0YWJsZTtcbiAgICAgICAgICAgIGNvbnN0IHN0YXRlID0gd3JpdGFibGUuX3N0YXRlO1xuICAgICAgICAgICAgaWYgKHN0YXRlID09PSAnZXJyb3JpbmcnKSB7XG4gICAgICAgICAgICAgICAgdGhyb3cgd3JpdGFibGUuX3N0b3JlZEVycm9yO1xuICAgICAgICAgICAgfVxuICAgICAgICAgICAgcmV0dXJuIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyUGVyZm9ybVRyYW5zZm9ybShjb250cm9sbGVyLCBjaHVuayk7XG4gICAgICAgIH0pO1xuICAgIH1cbiAgICByZXR1cm4gVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJQZXJmb3JtVHJhbnNmb3JtKGNvbnRyb2xsZXIsIGNodW5rKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQWJvcnRBbGdvcml0aG0oc3RyZWFtLCByZWFzb24pIHtcbiAgICAvLyBhYm9ydCgpIGlzIG5vdCBjYWxsZWQgc3luY2hyb25vdXNseSwgc28gaXQgaXMgcG9zc2libGUgZm9yIGFib3J0KCkgdG8gYmUgY2FsbGVkIHdoZW4gdGhlIHN0cmVhbSBpcyBhbHJlYWR5XG4gICAgLy8gZXJyb3JlZC5cbiAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcihzdHJlYW0sIHJlYXNvbik7XG4gICAgcmV0dXJuIHByb21pc2VSZXNvbHZlZFdpdGgodW5kZWZpbmVkKTtcbn1cbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTaW5rQ2xvc2VBbGdvcml0aG0oc3RyZWFtKSB7XG4gICAgLy8gc3RyZWFtLl9yZWFkYWJsZSBjYW5ub3QgY2hhbmdlIGFmdGVyIGNvbnN0cnVjdGlvbiwgc28gY2FjaGluZyBpdCBhY3Jvc3MgYSBjYWxsIHRvIHVzZXIgY29kZSBpcyBzYWZlLlxuICAgIGNvbnN0IHJlYWRhYmxlID0gc3RyZWFtLl9yZWFkYWJsZTtcbiAgICBjb25zdCBjb250cm9sbGVyID0gc3RyZWFtLl90cmFuc2Zvcm1TdHJlYW1Db250cm9sbGVyO1xuICAgIGNvbnN0IGZsdXNoUHJvbWlzZSA9IGNvbnRyb2xsZXIuX2ZsdXNoQWxnb3JpdGhtKCk7XG4gICAgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXJDbGVhckFsZ29yaXRobXMoY29udHJvbGxlcik7XG4gICAgLy8gUmV0dXJuIGEgcHJvbWlzZSB0aGF0IGlzIGZ1bGZpbGxlZCB3aXRoIHVuZGVmaW5lZCBvbiBzdWNjZXNzLlxuICAgIHJldHVybiB0cmFuc2Zvcm1Qcm9taXNlV2l0aChmbHVzaFByb21pc2UsICgpID0+IHtcbiAgICAgICAgaWYgKHJlYWRhYmxlLl9zdGF0ZSA9PT0gJ2Vycm9yZWQnKSB7XG4gICAgICAgICAgICB0aHJvdyByZWFkYWJsZS5fc3RvcmVkRXJyb3I7XG4gICAgICAgIH1cbiAgICAgICAgUmVhZGFibGVTdHJlYW1EZWZhdWx0Q29udHJvbGxlckNsb3NlKHJlYWRhYmxlLl9yZWFkYWJsZVN0cmVhbUNvbnRyb2xsZXIpO1xuICAgIH0sIHIgPT4ge1xuICAgICAgICBUcmFuc2Zvcm1TdHJlYW1FcnJvcihzdHJlYW0sIHIpO1xuICAgICAgICB0aHJvdyByZWFkYWJsZS5fc3RvcmVkRXJyb3I7XG4gICAgfSk7XG59XG4vLyBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0U291cmNlIEFsZ29yaXRobXNcbmZ1bmN0aW9uIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRTb3VyY2VQdWxsQWxnb3JpdGhtKHN0cmVhbSkge1xuICAgIC8vIEludmFyaWFudC4gRW5mb3JjZWQgYnkgdGhlIHByb21pc2VzIHJldHVybmVkIGJ5IHN0YXJ0KCkgYW5kIHB1bGwoKS5cbiAgICBUcmFuc2Zvcm1TdHJlYW1TZXRCYWNrcHJlc3N1cmUoc3RyZWFtLCBmYWxzZSk7XG4gICAgLy8gUHJldmVudCB0aGUgbmV4dCBwdWxsKCkgY2FsbCB1bnRpbCB0aGVyZSBpcyBiYWNrcHJlc3N1cmUuXG4gICAgcmV0dXJuIHN0cmVhbS5fYmFja3ByZXNzdXJlQ2hhbmdlUHJvbWlzZTtcbn1cbi8vIEhlbHBlciBmdW5jdGlvbnMgZm9yIHRoZSBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlci5cbmZ1bmN0aW9uIGRlZmF1bHRDb250cm9sbGVyQnJhbmRDaGVja0V4Y2VwdGlvbiQxKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgVHJhbnNmb3JtU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIucHJvdG90eXBlLiR7bmFtZX0gY2FuIG9ubHkgYmUgdXNlZCBvbiBhIFRyYW5zZm9ybVN0cmVhbURlZmF1bHRDb250cm9sbGVyYCk7XG59XG4vLyBIZWxwZXIgZnVuY3Rpb25zIGZvciB0aGUgVHJhbnNmb3JtU3RyZWFtLlxuZnVuY3Rpb24gc3RyZWFtQnJhbmRDaGVja0V4Y2VwdGlvbiQyKG5hbWUpIHtcbiAgICByZXR1cm4gbmV3IFR5cGVFcnJvcihgVHJhbnNmb3JtU3RyZWFtLnByb3RvdHlwZS4ke25hbWV9IGNhbiBvbmx5IGJlIHVzZWQgb24gYSBUcmFuc2Zvcm1TdHJlYW1gKTtcbn1cblxuZXhwb3J0IHsgQnl0ZUxlbmd0aFF1ZXVpbmdTdHJhdGVneSwgQ291bnRRdWV1aW5nU3RyYXRlZ3ksIFJlYWRhYmxlQnl0ZVN0cmVhbUNvbnRyb2xsZXIsIFJlYWRhYmxlU3RyZWFtLCBSZWFkYWJsZVN0cmVhbUJZT0JSZWFkZXIsIFJlYWRhYmxlU3RyZWFtQllPQlJlcXVlc3QsIFJlYWRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIsIFJlYWRhYmxlU3RyZWFtRGVmYXVsdFJlYWRlciwgVHJhbnNmb3JtU3RyZWFtLCBUcmFuc2Zvcm1TdHJlYW1EZWZhdWx0Q29udHJvbGxlciwgV3JpdGFibGVTdHJlYW0sIFdyaXRhYmxlU3RyZWFtRGVmYXVsdENvbnRyb2xsZXIsIFdyaXRhYmxlU3RyZWFtRGVmYXVsdFdyaXRlciB9O1xuLy8jIHNvdXJjZU1hcHBpbmdVUkw9cG9ueWZpbGwuZXMyMDE4Lm1qcy5tYXBcbiIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcImNyeXB0b1wiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiaHR0cFwiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwiaHR0cHNcIik7OyIsIm1vZHVsZS5leHBvcnRzID0gcmVxdWlyZShcInN0cmVhbVwiKTs7IiwibW9kdWxlLmV4cG9ydHMgPSByZXF1aXJlKFwidXJsXCIpOzsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJ1dGlsXCIpOzsiLCJtb2R1bGUuZXhwb3J0cyA9IHJlcXVpcmUoXCJ6bGliXCIpOzsiLCIvLyBUaGUgbW9kdWxlIGNhY2hlXG52YXIgX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fID0ge307XG5cbi8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG5mdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuXHRpZihfX3dlYnBhY2tfbW9kdWxlX2NhY2hlX19bbW9kdWxlSWRdKSB7XG5cdFx0cmV0dXJuIF9fd2VicGFja19tb2R1bGVfY2FjaGVfX1ttb2R1bGVJZF0uZXhwb3J0cztcblx0fVxuXHQvLyBDcmVhdGUgYSBuZXcgbW9kdWxlIChhbmQgcHV0IGl0IGludG8gdGhlIGNhY2hlKVxuXHR2YXIgbW9kdWxlID0gX193ZWJwYWNrX21vZHVsZV9jYWNoZV9fW21vZHVsZUlkXSA9IHtcblx0XHQvLyBubyBtb2R1bGUuaWQgbmVlZGVkXG5cdFx0Ly8gbm8gbW9kdWxlLmxvYWRlZCBuZWVkZWRcblx0XHRleHBvcnRzOiB7fVxuXHR9O1xuXG5cdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuXHRfX3dlYnBhY2tfbW9kdWxlc19fW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuXHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuXHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG59XG5cbiIsIi8vIG1vZHVsZSBleHBvcnRzIG11c3QgYmUgcmV0dXJuZWQgZnJvbSBydW50aW1lIHNvIGVudHJ5IGlubGluaW5nIGlzIGRpc2FibGVkXG4vLyBzdGFydHVwXG4vLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbnJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKDk5MCk7XG4iLCIvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9ucyBmb3IgaGFybW9ueSBleHBvcnRzXG5fX3dlYnBhY2tfcmVxdWlyZV9fLmQgPSAoZXhwb3J0cywgZGVmaW5pdGlvbikgPT4ge1xuXHRmb3IodmFyIGtleSBpbiBkZWZpbml0aW9uKSB7XG5cdFx0aWYoX193ZWJwYWNrX3JlcXVpcmVfXy5vKGRlZmluaXRpb24sIGtleSkgJiYgIV9fd2VicGFja19yZXF1aXJlX18ubyhleHBvcnRzLCBrZXkpKSB7XG5cdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywga2V5LCB7IGVudW1lcmFibGU6IHRydWUsIGdldDogZGVmaW5pdGlvbltrZXldIH0pO1xuXHRcdH1cblx0fVxufTsiLCJfX3dlYnBhY2tfcmVxdWlyZV9fLm8gPSAob2JqLCBwcm9wKSA9PiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqLCBwcm9wKSIsIi8vIGRlZmluZSBfX2VzTW9kdWxlIG9uIGV4cG9ydHNcbl9fd2VicGFja19yZXF1aXJlX18uciA9IChleHBvcnRzKSA9PiB7XG5cdGlmKHR5cGVvZiBTeW1ib2wgIT09ICd1bmRlZmluZWQnICYmIFN5bWJvbC50b1N0cmluZ1RhZykge1xuXHRcdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCBTeW1ib2wudG9TdHJpbmdUYWcsIHsgdmFsdWU6ICdNb2R1bGUnIH0pO1xuXHR9XG5cdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShleHBvcnRzLCAnX19lc01vZHVsZScsIHsgdmFsdWU6IHRydWUgfSk7XG59OyJdLCJzb3VyY2VSb290IjoiIn0= + +/***/ }), + +/***/ 622: +/***/ (function(module) { + +module.exports = require("path"); + +/***/ }), + +/***/ 669: +/***/ (function(module) { + +module.exports = require("util"); + +/***/ }), + +/***/ 747: +/***/ (function(module) { + +module.exports = require("fs"); + +/***/ }), + +/***/ 761: +/***/ (function(module) { + +module.exports = require("zlib"); + +/***/ }), + +/***/ 762: +/***/ (function(module) { + +// API +module.exports = abort; + +/** + * Aborts leftover active jobs + * + * @param {object} state - current state object + */ +function abort(state) +{ + Object.keys(state.jobs).forEach(clean.bind(state)); + + // reset leftover jobs + state.jobs = {}; +} + +/** + * Cleans up leftover job by invoking abort function for the provided job id + * + * @this state + * @param {string|number} key - job id to abort + */ +function clean(key) +{ + if (typeof this.jobs[key] == 'function') + { + this.jobs[key](); + } +} + + +/***/ }), + +/***/ 769: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; +/*! + * mime-types + * Copyright(c) 2014 Jonathan Ong + * Copyright(c) 2015 Douglas Christopher Wilson + * MIT Licensed + */ + + + +/** + * Module dependencies. + * @private + */ + +var db = __webpack_require__(128) +var extname = __webpack_require__(622).extname + +/** + * Module variables. + * @private + */ + +var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/ +var TEXT_TYPE_REGEXP = /^text\//i + +/** + * Module exports. + * @public + */ + +exports.charset = charset +exports.charsets = { lookup: charset } +exports.contentType = contentType +exports.extension = extension +exports.extensions = Object.create(null) +exports.lookup = lookup +exports.types = Object.create(null) + +// Populate the extensions/types maps +populateMaps(exports.extensions, exports.types) + +/** + * Get the default charset for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function charset (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + var mime = match && db[match[1].toLowerCase()] + + if (mime && mime.charset) { + return mime.charset + } + + // default text/* to utf-8 + if (match && TEXT_TYPE_REGEXP.test(match[1])) { + return 'UTF-8' + } + + return false +} + +/** + * Create a full Content-Type header given a MIME type or extension. + * + * @param {string} str + * @return {boolean|string} + */ + +function contentType (str) { + // TODO: should this even be in this module? + if (!str || typeof str !== 'string') { + return false + } + + var mime = str.indexOf('/') === -1 + ? exports.lookup(str) + : str + + if (!mime) { + return false + } + + // TODO: use content-type or other module + if (mime.indexOf('charset') === -1) { + var charset = exports.charset(mime) + if (charset) mime += '; charset=' + charset.toLowerCase() + } + + return mime +} + +/** + * Get the default extension for a MIME type. + * + * @param {string} type + * @return {boolean|string} + */ + +function extension (type) { + if (!type || typeof type !== 'string') { + return false + } + + // TODO: use media-typer + var match = EXTRACT_TYPE_REGEXP.exec(type) + + // get extensions + var exts = match && exports.extensions[match[1].toLowerCase()] + + if (!exts || !exts.length) { + return false + } + + return exts[0] +} + +/** + * Lookup the MIME type for a file path/extension. + * + * @param {string} path + * @return {boolean|string} + */ + +function lookup (path) { + if (!path || typeof path !== 'string') { + return false + } + + // get the extension ("ext" or ".ext" or full path) + var extension = extname('x.' + path) + .toLowerCase() + .substr(1) + + if (!extension) { + return false + } + + return exports.types[extension] || false +} + +/** + * Populate the extensions and types maps. + * @private + */ + +function populateMaps (extensions, types) { + // source preference (least -> most) + var preference = ['nginx', 'apache', undefined, 'iana'] + + Object.keys(db).forEach(function forEachMimeType (type) { + var mime = db[type] + var exts = mime.extensions + + if (!exts || !exts.length) { + return + } + + // mime -> extensions + extensions[type] = exts + + // extension -> mime + for (var i = 0; i < exts.length; i++) { + var extension = exts[i] + + if (types[extension]) { + var from = preference.indexOf(db[types[extension]].source) + var to = preference.indexOf(mime.source) + + if (types[extension] !== 'application/octet-stream' && + (from > to || (from === to && types[extension].substr(0, 12) === 'application/'))) { + // skip the remapping + continue + } + } + + // set the extension -> mime + types[extension] = type + } + }) +} + + +/***/ }), + +/***/ 792: +/***/ (function(module, __unusedexports, __webpack_require__) { + +var defer = __webpack_require__(154); + +// API +module.exports = async; + +/** + * Runs provided callback asynchronously + * even if callback itself is not + * + * @param {function} callback - callback to invoke + * @returns {function} - augmented callback + */ +function async(callback) +{ + var isAsync = false; + + // check if async happened + defer(function() { isAsync = true; }); + + return function async_callback(err, result) + { + if (isAsync) + { + callback(err, result); + } + else + { + defer(function nextTick_callback() + { + callback(err, result); + }); + } + }; +} + + +/***/ }), + +/***/ 835: +/***/ (function(module) { + +module.exports = require("url"); + +/***/ }) + +/******/ }); \ No newline at end of file diff --git a/.github/actions/send-email/index.js b/.github/actions/send-email/index.js new file mode 100644 index 0000000000..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..f75907db07 --- /dev/null +++ b/.github/actions/send-email/package-lock.json @@ -0,0 +1,173 @@ +{ + "name": "send-email", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@actions/core": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + }, + "@zeit/ncc": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.21.1.tgz", + "integrity": "sha512-M9WzgquSOt2nsjRkYM9LRylBLmmlwNCwYbm3Up3PDEshfvdmIfqpFNSK8EJvR18NwZjGHE5z2avlDtYQx2JQnw==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==" + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "fetch-blob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.1.tgz", + "integrity": "sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ==" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "ky": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.25.1.tgz", + "integrity": "sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA==" + }, + "ky-universal": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.8.2.tgz", + "integrity": "sha512-xe0JaOH9QeYxdyGLnzUOVGK4Z6FGvDVzcXFTdrYA1f33MZdEa45sUDaMBy98xQMcsd2XIBrTXRrRYnegcSdgVQ==", + "requires": { + "abort-controller": "^3.0.0", + "node-fetch": "3.0.0-beta.9" + } + }, + "mailgun.js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/mailgun.js/-/mailgun.js-3.3.0.tgz", + "integrity": "sha512-Ikcl9Lp18oXu8/ht6Ow3b2yRYBEa4S70YvquOM2O4FA4NDboa8btZIMEQRRFAmBEfEbWOPrl/Z6E8kL8vnyonQ==", + "requires": { + "bluebird": "^3.7.2", + "btoa": "^1.1.2", + "ky": "^0.25.1", + "ky-universal": "^0.8.2", + "url": "^0.11.0", + "url-join": "0.0.1", + "web-streams-polyfill": "^3.0.1", + "webpack-merge": "^5.4.0" + } + }, + "node-fetch": { + "version": "3.0.0-beta.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", + "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "requires": { + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^2.1.1" + }, + "dependencies": { + "data-uri-to-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" + } + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "requires": { + "kind-of": "^6.0.2" + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "url-join": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-0.0.1.tgz", + "integrity": "sha1-HbSK1CLTQCRpqH99l73r/k+x48g=" + }, + "web-streams-polyfill": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.0.2.tgz", + "integrity": "sha512-JTNkNbAKoSo8NKiqu2UUaqRFCDWWZaCOsXuJEsToWopikTA0YHKKUf91GNkS/SnD8JixOkJjVsiacNlrFnRECA==" + }, + "webpack-merge": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz", + "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==", + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==" + } + } +} diff --git a/.github/actions/send-email/package.json b/.github/actions/send-email/package.json new file mode 100644 index 0000000000..678a63992a --- /dev/null +++ b/.github/actions/send-email/package.json @@ -0,0 +1,23 @@ +{ + "name": "send-email", + "version": "1.0.0", + "description": "Send Emails from GitHub Actions workflows using Mailgun.", + "main": "index.js", + "scripts": { + "pack": "ncc build" + }, + "keywords": [ + "Firebase", + "Release", + "Automation" + ], + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "dependencies": { + "@actions/core": "^1.2.6", + "mailgun.js": "^3.3.0" + }, + "devDependencies": { + "@zeit/ncc": "^0.21.1" + } +} diff --git a/.github/actions/send-tweet/package-lock.json b/.github/actions/send-tweet/package-lock.json index e59bab6c1d..6caa0a69f5 100644 --- a/.github/actions/send-tweet/package-lock.json +++ b/.github/actions/send-tweet/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@actions/core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.2.tgz", - "integrity": "sha512-IbCx7oefq+Gi6FWbSs2Fnw8VkEI6Y4gvjrYprY3RV//ksq/KPMlClOerJ4jRosyal6zkUIc8R9fS/cpRMlGClg==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", + "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" }, "@zeit/ncc": { "version": "0.21.1", diff --git a/.github/actions/send-tweet/package.json b/.github/actions/send-tweet/package.json index 0f22b3fa1d..440567dc26 100644 --- a/.github/actions/send-tweet/package.json +++ b/.github/actions/send-tweet/package.json @@ -14,7 +14,7 @@ "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "dependencies": { - "@actions/core": "^1.2.2", + "@actions/core": "^1.2.6", "twitter": "^1.7.1" }, "devDependencies": { 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/scripts/run_integration_tests.sh b/.github/scripts/run_integration_tests.sh index fd479df552..37dc7d1216 100755 --- a/.github/scripts/run_integration_tests.sh +++ b/.github/scripts/run_integration_tests.sh @@ -22,4 +22,6 @@ gpg --quiet --batch --yes --decrypt --passphrase="${FIREBASE_SERVICE_ACCT_KEY}" echo "${FIREBASE_API_KEY}" > test/resources/apikey.txt +echo "${FIREBASE_APP_ID}" > test/resources/appid.txt + npm run test:integration -- --updateRules --testMultiTenancy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9461c44f43..244d123dd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - node-version: [8.x, 10.x] + node-version: [10.x, 12.x, 14.x] steps: - uses: actions/checkout@v1 @@ -17,9 +17,17 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - name: Install, build and test + - name: Install and build run: | npm ci npm run build npm run build:tests - npm test + - name: Lint and run unit tests + run: npm test + - name: Run api-extractor + run: npm run api-extractor + - name: Run emulator-based integration tests + run: | + npm install -g firebase-tools + firebase emulators:exec --project fake-project-id --only auth,database,firestore \ + 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml new file mode 100644 index 0000000000..536827b1e5 --- /dev/null +++ b/.github/workflows/nightly.yml @@ -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. + +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@v2 + with: + ref: ${{ github.event.client_payload.ref || github.ref }} + + - name: Set up Node.js + uses: actions/setup-node@v1 + with: + node-version: 10.x + + - name: Install and build + run: | + npm ci + npm run build + npm run build:tests + + - name: Run unit tests + run: npm test + + - name: Verify public API + run: npm run api-extractor + + - name: Run integration tests + run: ./.github/scripts/run_integration_tests.sh + env: + FIREBASE_SERVICE_ACCT_KEY: ${{ secrets.FIREBASE_SERVICE_ACCT_KEY }} + FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }} + 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 index dbe21dc5e4..d2a2797765 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,7 +47,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 8.x + node-version: 10.x - name: Install and build run: | @@ -58,11 +58,15 @@ jobs: - name: Run unit tests run: npm test + - name: Verify public API + run: npm run api-extractor + - name: Run integration tests run: ./.github/scripts/run_integration_tests.sh env: 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: | @@ -112,7 +116,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v1 with: - node-version: 8.x + node-version: 10.x - name: Publish preflight check id: preflight diff --git a/.gitignore b/.gitignore index 814fb22453..4c60db05ce 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log lib/ .tmp/ +temp/ typings/ coverage/ node_modules/ @@ -13,6 +14,7 @@ node_modules/ # Real key file should not be checked in test/resources/key.json test/resources/apikey.txt +test/resources/appid.txt # Release tarballs should not be checked in firebase-admin-*.tgz diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e263ffadc2..a3ad29f277 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,7 +87,7 @@ information on using pull requests. ### Prerequisites -1. Node.js 8.13.0 or higher. +1. Node.js 10.13.0 or higher. 2. NPM 5 or higher (NPM 6 recommended). 3. Google Cloud SDK ([`gcloud`](https://cloud.google.com/sdk/downloads) utility) @@ -123,6 +123,8 @@ There are two test suites: unit and integration. The unit test suite is intended development, and the integration test suite is intended to be run before packaging up release candidates. +#### Unit Tests + To run the unit test suite: ```bash @@ -135,7 +137,26 @@ If you wish to skip the linter, and only run the unit tests: $ npm run test:unit ``` -The integration tests run against an actual Firebase project. Create a new +#### Integration Tests with Emulator Suite + +Some of the integration tests work with the Emulator Suite and you can run them +without an actual Firebase project. + +First, make sure to [install Firebase CLI](https://firebase.google.com/docs/cli#install_the_firebase_cli). +And then: + +```bash + firebase emulators:exec --project fake-project-id --only auth,database,firestore \ + 'npx mocha \"test/integration/{auth,database,firestore}.spec.ts\" --slow 5000 --timeout 20000 --require ts-node/register' +``` + +Currently, only the Auth, Database, and Firestore test suites work. Some test +cases will be automatically skipped due to lack of emulator support. The section +below covers how to run the full test suite against an actual Firebase project. + +#### Integration Tests with an actual Firebase project + +Other integration tests require an actual Firebase project. Create a new project in the [Firebase Console](https://console.firebase.google.com), if you do not already have one suitable for running the tests against. Then obtain the following credentials from the project: @@ -146,7 +167,7 @@ following credentials from the project: 2. *Web API key*: This is displayed in the "Settings > General" tab of the console. Copy it and save to a new text file at `test/resources/apikey.txt`. -Then set up your Firebase/GCP project as follows: +Then set up your Firebase/Google Cloud project as follows: 1. Enable Firestore: Go to the Firebase Console, and select "Database" from the "Develop" menu. Click on the "Create database" button. You may choose @@ -160,15 +181,15 @@ Then set up your Firebase/GCP project as follows: https://console.developers.google.com/apis/api/firebaseml.googleapis.com/overview) and make sure your project is selected. If the API is not already enabled, click Enable. 4. Enable the IAM API: Go to the - [Google Cloud Platform Console](https://console.cloud.google.com) and make - sure your Firebase/GCP project is selected. Select "APIs & Services > + [Google Cloud Console](https://console.cloud.google.com) and make + sure your Firebase/Google Cloud project is selected. Select "APIs & Services > Dashboard" from the main menu, and click the "ENABLE APIS AND SERVICES" button. Search for and enable the "Identity and Access Management (IAM) API". 5. Grant your service account the 'Firebase Authentication Admin' role. This is required to ensure that exported user records contain the password hashes of the user accounts: - 1. Go to [Google Cloud Platform Console / IAM & admin](https://console.cloud.google.com/iam-admin). + 1. Go to [Google Cloud Console / IAM & admin](https://console.cloud.google.com/iam-admin). 2. Find your service account in the list, and click the 'pencil' icon to edit it's permissions. 3. Click 'ADD ANOTHER ROLE' and choose 'Firebase Authentication Admin'. 4. Click 'SAVE'. diff --git a/README.md b/README.md index d7155fb512..59a22770ff 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ requests, code review feedback, and also pull requests. ## Supported Environments -We support Node.js 8.13.0 and higher. +We support Node.js 10.13.0 and higher. Please also note that the Admin SDK should only be used in server-side/back-end environments controlled by the app developer. diff --git a/api-extractor.json b/api-extractor.json new file mode 100644 index 0000000000..140b645cfe --- /dev/null +++ b/api-extractor.json @@ -0,0 +1,368 @@ +/** + * Config file for API Extractor. For more info, please visit: https://api-extractor.com + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + /** + * Optionally specifies another JSON config file that this file extends from. This provides a way for + * standard settings to be shared across multiple projects. + * + * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains + * the "extends" field. Otherwise, the first path segment is interpreted as an NPM package name, and will be + * resolved using NodeJS require(). + * + * SUPPORTED TOKENS: none + * DEFAULT VALUE: "" + */ + // "extends": "./shared/api-extractor-base.json" + // "extends": "my-package/include/api-extractor-base.json" + + /** + * Determines the "" token that can be used with other config file settings. The project folder + * typically contains the tsconfig.json and package.json config files, but the path is user-defined. + * + * The path is resolved relative to the folder of the config file that contains the setting. + * + * The default value for "projectFolder" is the token "", which means the folder is determined by traversing + * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder + * that contains a tsconfig.json file. If a tsconfig.json file cannot be found in this way, then an error + * will be reported. + * + * SUPPORTED TOKENS: + * DEFAULT VALUE: "" + */ + // "projectFolder": "..", + + /** + * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis. API Extractor + * analyzes the symbols exported by this module. + * + * The file extension must be ".d.ts" and not ".ts". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + */ + // We point to the firebase-namespace.d.ts file since index.d.ts uses namespace imports that are + // not supported by API Extractor. See https://github.com/microsoft/rushstack/issues/1029 and + // https://github.com/microsoft/rushstack/issues/2338. + "mainEntryPointFilePath": "/lib/firebase-namespace.d.ts", + + /** + * A list of NPM package names whose exports should be treated as part of this package. + * + * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1", + * and another NPM package "library2" is embedded in this bundle. Some types from library2 may become part + * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly + * imports library2. To avoid this, we can specify: + * + * "bundledPackages": [ "library2" ], + * + * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been + * local files for library1. + */ + "bundledPackages": [], + + /** + * Determines how the TypeScript compiler engine will be invoked by API Extractor. + */ + "compiler": { + /** + * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * Note: This setting will be ignored if "overrideTsconfig" is used. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/tsconfig.json" + */ + // "tsconfigFilePath": "/tsconfig.json", + /** + * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk. + * The object must conform to the TypeScript tsconfig schema: + * + * http://json.schemastore.org/tsconfig + * + * If omitted, then the tsconfig.json file will be read from the "projectFolder". + * + * DEFAULT VALUE: no overrideTsconfig section + */ + // "overrideTsconfig": { + // . . . + // } + /** + * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended + * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when + * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses + * for its analysis. Where possible, the underlying issue should be fixed rather than relying on skipLibCheck. + * + * DEFAULT VALUE: false + */ + // "skipLibCheck": true, + }, + + /** + * Configures how the API report file (*.api.md) will be generated. + */ + "apiReport": { + /** + * (REQUIRED) Whether to generate an API report. + */ + "enabled": true + + /** + * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce + * a full file path. + * + * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/". + * + * SUPPORTED TOKENS: , + * DEFAULT VALUE: ".api.md" + */ + // "reportFileName": ".api.md", + + /** + * Specifies the folder where the API report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * The API report file is normally tracked by Git. Changes to it can be used to trigger a branch policy, + * e.g. for an API review. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/etc/" + */ + // "reportFolder": "/etc/", + + /** + * Specifies the folder where the temporary report file is written. The file name portion is determined by + * the "reportFileName" setting. + * + * After the temporary file is written to disk, it is compared with the file in the "reportFolder". + * If they are different, a production build will fail. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/" + */ + // "reportTempFolder": "/temp/" + }, + + /** + * Configures how the doc model file (*.api.json) will be generated. + */ + "docModel": { + /** + * (REQUIRED) Whether to generate a doc model file. + */ + "enabled": true + + /** + * The output path for the doc model file. The file extension should be ".api.json". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/temp/.api.json" + */ + // "apiJsonFilePath": "/temp/.api.json" + }, + + /** + * Configures how the .d.ts rollup file will be generated. + */ + "dtsRollup": { + /** + * (REQUIRED) Whether to generate the .d.ts rollup file. + */ + "enabled": false + + /** + * Specifies the output path for a .d.ts rollup file to be generated without any trimming. + * This file will include all declarations that are exported by the main entry point. + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "/dist/.d.ts" + */ + // "untrimmedFilePath": "/dist/.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release. + * This file will include only declarations that are marked as "@public" or "@beta". + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "betaTrimmedFilePath": "/dist/-beta.d.ts", + + /** + * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release. + * This file will include only declarations that are marked as "@public". + * + * If the path is an empty string, then this file will not be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "publicTrimmedFilePath": "/dist/-public.d.ts", + + /** + * When a declaration is trimmed, by default it will be replaced by a code comment such as + * "Excluded from this release type: exampleMember". Set "omitTrimmingComments" to true to remove the + * declaration completely. + * + * DEFAULT VALUE: false + */ + // "omitTrimmingComments": true + }, + + /** + * Configures how the tsdoc-metadata.json file will be generated. + */ + "tsdocMetadata": { + /** + * Whether to generate the tsdoc-metadata.json file. + * + * DEFAULT VALUE: true + */ + // "enabled": true, + /** + * Specifies where the TSDoc metadata file should be written. + * + * The path is resolved relative to the folder of the config file that contains the setting; to change this, + * prepend a folder token such as "". + * + * The default value is "", which causes the path to be automatically inferred from the "tsdocMetadata", + * "typings" or "main" fields of the project's package.json. If none of these fields are set, the lookup + * falls back to "tsdoc-metadata.json" in the package folder. + * + * SUPPORTED TOKENS: , , + * DEFAULT VALUE: "" + */ + // "tsdocMetadataFilePath": "/dist/tsdoc-metadata.json" + }, + + /** + * Specifies what type of newlines API Extractor should use when writing output files. By default, the output files + * will be written with Windows-style newlines. To use POSIX-style newlines, specify "lf" instead. + * To use the OS's default newline kind, specify "os". + * + * DEFAULT VALUE: "crlf" + */ + // "newlineKind": "crlf", + + /** + * Configures how API Extractor reports error and warning messages produced during analysis. + * + * There are three sources of messages: compiler messages, API Extractor messages, and TSDoc messages. + */ + "messages": { + /** + * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing + * the input .d.ts files. + * + * TypeScript message identifiers start with "TS" followed by an integer. For example: "TS2551" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "compilerMessageReporting": { + /** + * Configures the default routing for messages that don't match an explicit rule in this table. + */ + "default": { + /** + * Specifies whether the message should be written to the the tool's output log. Note that + * the "addToApiReportFile" property may supersede this option. + * + * Possible values: "error", "warning", "none" + * + * Errors cause the build to fail and return a nonzero exit code. Warnings cause a production build fail + * and return a nonzero exit code. For a non-production build (e.g. when "api-extractor run" includes + * the "--local" option), the warning is displayed but the build will not fail. + * + * DEFAULT VALUE: "warning" + */ + "logLevel": "warning" + + /** + * When addToApiReportFile is true: If API Extractor is configured to write an API report file (.api.md), + * then the message will be written inside that file; otherwise, the message is instead logged according to + * the "logLevel" option. + * + * DEFAULT VALUE: false + */ + // "addToApiReportFile": false + } + + // "TS2551": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + }, + + /** + * Configures handling of messages reported by API Extractor during its analysis. + * + * API Extractor message identifiers start with "ae-". For example: "ae-extra-release-tag" + * + * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings + */ + "extractorMessageReporting": { + "default": { + "logLevel": "warning" + // "addToApiReportFile": false + }, + + "ae-missing-release-tag": { + "logLevel": "none" + }, + + "ae-unresolved-link": { + "logLevel": "none" + } + }, + + /** + * Configures handling of messages reported by the TSDoc parser when analyzing code comments. + * + * TSDoc message identifiers start with "tsdoc-". For example: "tsdoc-link-tag-unescaped-text" + * + * DEFAULT VALUE: A single "default" entry with logLevel=warning. + */ + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + // "addToApiReportFile": false + } + + // "tsdoc-link-tag-unescaped-text": { + // "logLevel": "warning", + // "addToApiReportFile": true + // }, + // + // . . . + } + } +} diff --git a/docgen/content-sources/node/HOME.md b/docgen/content-sources/node/HOME.md index bf22c94da8..4e253f7733 100644 --- a/docgen/content-sources/node/HOME.md +++ b/docgen/content-sources/node/HOME.md @@ -1,6 +1,6 @@ # Firebase Admin Node.js SDK Reference -The Admin SDK lets you interact with Firebase from privileged environments. +The Admin SDK is a set of server libraries that lets you interact with Firebase from privileged environments. You can install it via our [npm package](https://www.npmjs.com/package/firebase-admin). To get started using the Firebase Admin Node.js SDK, see diff --git a/docgen/content-sources/node/toc.yaml b/docgen/content-sources/node/toc.yaml index 6af80e2356..c563a522ba 100644 --- a/docgen/content-sources/node/toc.yaml +++ b/docgen/content-sources/node/toc.yaml @@ -17,29 +17,49 @@ toc: path: /docs/reference/admin/node/admin.app section: - title: "App" - path: /docs/reference/admin/node/admin.app.App + path: /docs/reference/admin/node/admin.app.App-1 + +- title: "admin.appCheck" + path: /docs/reference/admin/node/admin.appCheck + section: + - title: "AppCheck" + path: /docs/reference/admin/node/admin.appCheck.AppCheck-1 + - title: "AppCheckToken" + path: /docs/reference/admin/node/admin.appCheck.AppCheckToken + - title: "AppCheckTokenOptions" + path: /docs/reference/admin/node/admin.appCheck.AppCheckTokenOptions + - title: "DecodedAppCheckToken" + path: /docs/reference/admin/node/admin.appCheck.DecodedAppCheckToken + - title: "VerifyAppCheckTokenResponse" + path: /docs/reference/admin/node/admin.appCheck.VerifyAppCheckTokenResponse - title: "admin.auth" path: /docs/reference/admin/node/admin.auth section: - title: "Auth" - path: /docs/reference/admin/node/admin.auth.Auth + path: /docs/reference/admin/node/admin.auth.Auth-1 - title: "ActionCodeSettings" path: /docs/reference/admin/node/admin.auth.ActionCodeSettings - - title: "AuthProviderConfig" - path: /docs/reference/admin/node/admin.auth.AuthProviderConfig - title: "AuthProviderConfigFilter" path: /docs/reference/admin/node/admin.auth.AuthProviderConfigFilter - - title: "CreateMultiFactorInfoRequest" - path: /docs/reference/admin/node/admin.auth.CreateMultiFactorInfoRequest + - title: "BaseAuthProviderConfig" + path: /docs/reference/admin/node/admin.auth.BaseAuthProviderConfig + - title: "BaseCreateMultiFactorInfoRequest" + path: /docs/reference/admin/node/admin.auth.BaseCreateMultiFactorInfoRequest + - title: "BaseUpdateMultiFactorInfoRequest" + path: /docs/reference/admin/node/admin.auth.BaseUpdateMultiFactorInfoRequest - title: "CreatePhoneMultiFactorInfoRequest" path: /docs/reference/admin/node/admin.auth.CreatePhoneMultiFactorInfoRequest - title: "CreateRequest" path: /docs/reference/admin/node/admin.auth.CreateRequest + - title: "EmailSignInProviderConfig" + path: /docs/reference/admin/node/admin.auth.EmailSignInProviderConfig - title: "ListProviderConfigResults" path: /docs/reference/admin/node/admin.auth.ListProviderConfigResults - title: "ListTenantsResult" path: /docs/reference/admin/node/admin.auth.ListTenantsResult + - title: "MultiFactorConfig" + path: /docs/reference/admin/node/admin.auth.MultiFactorConfig - title: "MultiFactorCreateSettings" path: /docs/reference/admin/node/admin.auth.MultiFactorCreateSettings - title: "MultiFactorInfo" @@ -48,6 +68,8 @@ toc: path: /docs/reference/admin/node/admin.auth.MultiFactorSettings - title: "MultiFactorUpdateSettings" path: /docs/reference/admin/node/admin.auth.MultiFactorUpdateSettings + - title: "OAuthResponseType" + path: /docs/reference/admin/node/admin.auth.OAuthResponseType - title: "OIDCAuthProviderConfig" path: /docs/reference/admin/node/admin.auth.OIDCAuthProviderConfig - title: "OIDCUpdateAuthProviderRequest" @@ -64,8 +86,6 @@ toc: path: /docs/reference/admin/node/admin.auth.TenantAwareAuth - title: "TenantManager" path: /docs/reference/admin/node/admin.auth.TenantManager - - title: "UpdateMultiFactorInfoRequest" - path: /docs/reference/admin/node/admin.auth.UpdateMultiFactorInfoRequest - title: "UpdatePhoneMultiFactorInfoRequest" path: /docs/reference/admin/node/admin.auth.UpdatePhoneMultiFactorInfoRequest - title: "UpdateRequest" @@ -90,6 +110,8 @@ toc: path: /docs/reference/admin/node/admin.auth.UserProviderRequest - title: "UserRecord" path: /docs/reference/admin/node/admin.auth.UserRecord + - title: "UserProvider" + path: /docs/reference/admin/node/admin.auth.UserProvider - title: "SessionCookieOptions" path: /docs/reference/admin/node/admin.auth.SessionCookieOptions - title: "BaseAuth" @@ -113,34 +135,28 @@ toc: path: /docs/reference/admin/node/admin.credential section: - title: "Credential" - path: /docs/reference/admin/node/admin.credential.Credential + path: /docs/reference/admin/node/admin.credential.Credential-1 - title: "admin.database" path: /docs/reference/admin/node/admin.database section: - title: "Database" - path: /docs/reference/admin/node/admin.database.Database - - title: "DataSnapshot" - path: /docs/reference/admin/node/admin.database.DataSnapshot - - title: "OnDisconnect" - path: /docs/reference/admin/node/admin.database.OnDisconnect - - title: "Query" - path: /docs/reference/admin/node/admin.database.Query - - title: "Reference" - path: /docs/reference/admin/node/admin.database.Reference - - title: "ServerValue" - path: /docs/reference/admin/node/admin.database.ServerValue - - title: "ThenableReference" - path: /docs/reference/admin/node/admin.database.ThenableReference + path: /docs/reference/admin/node/admin.database.Database-1 - title: "admin.firestore" path: /docs/reference/admin/node/admin.firestore +- title: "admin.installations" + path: /docs/reference/admin/node/admin.installations + section: + - title: "Installations" + path: /docs/reference/admin/node/admin.installations.Installations-1 + - title: "admin.instanceId" path: /docs/reference/admin/node/admin.instanceId section: - title: "InstanceId" - path: /docs/reference/admin/node/admin.instanceId.InstanceId + path: /docs/reference/admin/node/admin.instanceId.InstanceId-1 - title: "admin.machineLearning" path: /docs/reference/admin/node/admin.machineLearning @@ -150,23 +166,29 @@ toc: - title: "ListModelsResult" path: /docs/reference/admin/node/admin.machineLearning.ListModelsResult - title: "MachineLearning" - path: /docs/reference/admin/node/admin.machineLearning.MachineLearning + path: /docs/reference/admin/node/admin.machineLearning.MachineLearning-1 - title: "Model" path: /docs/reference/admin/node/admin.machineLearning.Model - - title: "ModelOptions" - path: /docs/reference/admin/node/admin.machineLearning.ModelOptions + - title: "ModelOptionsBase" + path: /docs/reference/admin/node/admin.machineLearning.ModelOptionsBase + - title: "GcsTfliteModelOptions" + path: /docs/reference/admin/node/admin.machineLearning.GcsTfliteModelOptions + - title: "AutoMLTfliteModelOptions" + path: /docs/reference/admin/node/admin.machineLearning.AutoMLTfliteModelOptions - title: "TFLiteModel" path: /docs/reference/admin/node/admin.machineLearning.TFLiteModel - title: "admin.messaging" path: /docs/reference/admin/node/admin.messaging section: + - title: "BaseMessage" + path: /docs/reference/admin/node/admin.messaging.BaseMessage - title: "TopicMessage" - path: /docs/reference/admin/node/TopicMessage + path: /docs/reference/admin/node/admin.messaging.TopicMessage - title: "TokenMessage" - path: /docs/reference/admin/node/TokenMessage + path: /docs/reference/admin/node/admin.messaging.TokenMessage - title: "ConditionMessage" - path: /docs/reference/admin/node/ConditionMessage + path: /docs/reference/admin/node/admin.messaging.ConditionMessage - title: "AndroidConfig" path: /docs/reference/admin/node/admin.messaging.AndroidConfig - title: "AndroidFcmOptions" @@ -178,7 +200,7 @@ toc: - title: "LightSettings" path: /docs/reference/admin/node/admin.messaging.LightSettings - title: "Messaging" - path: /docs/reference/admin/node/admin.messaging.Messaging + path: /docs/reference/admin/node/admin.messaging.Messaging-1 - title: "MessagingConditionResponse" path: /docs/reference/admin/node/admin.messaging.MessagingConditionResponse - title: "MessagingDeviceGroupResponse" @@ -242,7 +264,7 @@ toc: - title: "IosAppMetadata" path: /docs/reference/admin/node/admin.projectManagement.IosAppMetadata - title: "ProjectManagement" - path: /docs/reference/admin/node/admin.projectManagement.ProjectManagement + path: /docs/reference/admin/node/admin.projectManagement.ProjectManagement-1 - title: "ShaCertificate" path: /docs/reference/admin/node/admin.projectManagement.ShaCertificate @@ -258,19 +280,19 @@ toc: - title: "RulesetMetadataList" path: /docs/reference/admin/node/admin.securityRules.RulesetMetadataList - title: "SecurityRules" - path: /docs/reference/admin/node/admin.securityRules.SecurityRules + path: /docs/reference/admin/node/admin.securityRules.SecurityRules-1 - title: "admin.storage" path: /docs/reference/admin/node/admin.storage section: - title: "Storage" - path: /docs/reference/admin/node/admin.storage.Storage + path: /docs/reference/admin/node/admin.storage.Storage-1 - title: "admin.remoteConfig" path: /docs/reference/admin/node/admin.remoteConfig section: - title: "RemoteConfig" - path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfig + path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfig-1 - title: "RemoteConfigTemplate" path: /docs/reference/admin/node/admin.remoteConfig.RemoteConfigTemplate - title: "RemoteConfigParameter" diff --git a/docgen/generate-docs.js b/docgen/generate-docs.js index bc04538bfb..619526dff8 100644 --- a/docgen/generate-docs.js +++ b/docgen/generate-docs.js @@ -25,10 +25,16 @@ const yaml = require('js-yaml'); const repoPath = path.resolve(`${__dirname}/..`); +const defaultSources = [ + `${repoPath}/lib/firebase-namespace.d.ts`, + `${repoPath}/lib/firebase-namespace-api.d.ts`, + `${repoPath}/lib/**/*.d.ts`, +]; + // Command-line options. const { source: sourceFile } = yargs .option('source', { - default: `${repoPath}/src/*.d.ts`, + default: defaultSources.join(' '), describe: 'Typescript source file(s)', type: 'string' }) @@ -40,7 +46,11 @@ const contentPath = path.resolve(`${__dirname}/content-sources/node`); const tempHomePath = path.resolve(`${contentPath}/HOME_TEMP.md`); const devsitePath = `/docs/reference/admin/node/`; -const firestoreExcludes = ['v1', 'v1beta1', 'setLogFunction','DocumentData']; +const firestoreExcludes = [ + 'v1', 'v1beta1', 'setLogFunction','DocumentData', + 'BulkWriterOptions', 'DocumentChangeType', 'FirestoreDataConverter', + 'GrpcStatus', 'Precondition', 'ReadOptions', 'UpdateData', 'Settings', +]; const firestoreHtmlPath = `${docPath}/admin.firestore.html`; const firestoreHeader = `

Type aliases

@@ -51,6 +61,17 @@ const firestoreHeader = `
    `; const firestoreFooter = '\n
\n
\n'; +const databaseExcludes = ['enableLogging']; +const databaseHtmlPath = `${docPath}/admin.database.html`; +const databaseHeader = `
+

Type aliases

+
+

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

+
+
    `; +const databaseFooter = '\n
\n
\n'; + /** * Strips path prefix and returns only filename. * @param {string} path @@ -96,10 +117,11 @@ function fixLinks(file) { return fs.readFile(file, 'utf8').then(data => { const flattenedLinks = data .replace(/\.\.\//g, '') + .replace(/globals\.html/g, 'admin.html') .replace(/(modules|interfaces|classes|enums)\//g, ''); let caseFixedLinks = flattenedLinks; for (const lower in lowerToUpperLookup) { - const re = new RegExp(lower, 'g'); + const re = new RegExp('\\b' + lower, 'g'); caseFixedLinks = caseFixedLinks.replace(re, lowerToUpperLookup[lower]); } return fs.writeFile(file, caseFixedLinks); @@ -119,7 +141,7 @@ function generateTempHomeMdFile(tocRaw, homeRaw) { const { toc } = yaml.safeLoad(tocRaw); let tocPageLines = [homeRaw, '# API Reference']; toc.forEach(group => { - tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)})`); + tocPageLines.push(`\n## [${group.title}](${stripPath(group.path)}.html)`); const section = group.section || []; section.forEach(item => { tocPageLines.push(`- [${item.title}](${stripPath(item.path)}.html)`); @@ -150,13 +172,13 @@ function checkForMissingFilesAndFixFilenameCase() { // Preferred filename for devsite should be capitalized and taken from // toc.yaml. const tocFilePath = `${docPath}/${filename}.html`; - // Generated filename from Typedoc will be lowercase. - const generatedFilePath = `${docPath}/${filename.toLowerCase()}.html`; + // Generated filename from Typedoc will be lowercase and won't have the admin prefix. + const generatedFilePath = `${docPath}/${filename.toLowerCase().replace('admin.', '')}.html`; return fs.exists(generatedFilePath).then(exists => { if (exists) { // Store in a lookup table for link fixing. lowerToUpperLookup[ - `${filename.toLowerCase()}.html` + `${filename.toLowerCase().replace('admin.', '')}.html` ] = `${filename}.html`; return fs.rename(generatedFilePath, tocFilePath); } else { @@ -167,6 +189,7 @@ function checkForMissingFilesAndFixFilenameCase() { } }); }); + return Promise.all(fileCheckPromises).then(() => filenames); } @@ -253,15 +276,28 @@ function fixAllLinks(htmlFiles) { * Updates the auto-generated Firestore API references page, by appending * the specified HTML content block. * + * @param {string} htmlPath Path of the HTML file to update. * @param {string} contentBlock The HTML content block to be added to the Firestore docs. */ -function updateFirestoreHtml(contentBlock) { - const dom = new jsdom.JSDOM(fs.readFileSync(firestoreHtmlPath)); +function updateHtml(htmlPath, contentBlock) { + const dom = new jsdom.JSDOM(fs.readFileSync(htmlPath)); const contentNode = dom.window.document.body.querySelector('.col-12'); + // Recent versions of Typedoc generates an additional index section and a variables + // section for namespaces with re-exports. We iterate through these nodes and remove + // them from the output. + const sections = []; + contentNode.childNodes.forEach((child) => { + if (child.nodeName === 'SECTION') { + sections.push(child); + } + }); + contentNode.removeChild(sections[1]); + contentNode.removeChild(sections[2]); + const newSection = new jsdom.JSDOM(contentBlock); contentNode.appendChild(newSection.window.document.body.firstChild); - fs.writeFileSync(firestoreHtmlPath, dom.window.document.documentElement.outerHTML); + fs.writeFileSync(htmlPath, dom.window.document.documentElement.outerHTML); } /** @@ -272,7 +308,7 @@ function updateFirestoreHtml(contentBlock) { */ function addFirestoreTypeAliases() { return new Promise((resolve, reject) => { - const fileStream = fs.createReadStream(`${repoPath}/src/index.d.ts`); + const fileStream = fs.createReadStream(`${repoPath}/lib/firestore/index.d.ts`); fileStream.on('error', (err) => { reject(err); }); @@ -297,7 +333,49 @@ function addFirestoreTypeAliases() { lineReader.on('close', () => { try { contentBlock += firestoreFooter; - updateFirestoreHtml(contentBlock); + updateHtml(firestoreHtmlPath, contentBlock); + resolve(); + } catch (err) { + reject(err); + } + }); + }); +} + +/** + * Adds RTDB type aliases to the auto-generated API docs. These are the + * types that are imported from the @firebase/database package, and + * then re-exported from the admin.database namespace. Typedoc currently + * does not handle these correctly, so we need this solution instead. + */ +function addDatabaseTypeAliases() { + return new Promise((resolve, reject) => { + const fileStream = fs.createReadStream(`${repoPath}/lib/database/index.d.ts`); + fileStream.on('error', (err) => { + reject(err); + }); + const lineReader = readline.createInterface({ + input: fileStream, + }); + + let contentBlock = databaseHeader; + lineReader.on('line', (line) => { + line = line.trim(); + if (line.startsWith('export import') && line.indexOf('rtdb.') >= 0) { + const typeName = line.split(' ')[2]; + if (databaseExcludes.indexOf(typeName) === -1) { + contentBlock += ` +
  • + ${typeName} +
  • `; + } + } + }); + + lineReader.on('close', () => { + try { + contentBlock += databaseFooter; + updateHtml(databaseHtmlPath, contentBlock); resolve(); } catch (err) { reject(err); @@ -349,6 +427,8 @@ Promise.all([ moveFilesToRoot('enums'), ]); }) + // Rename the globals file to be the top-level admin doc. + .then(() => fs.rename(`${docPath}/globals.html`, `${docPath}/admin.html`)) // Check for files listed in TOC that are missing and warn if so. // Not blocking. .then(checkForMissingFilesAndFixFilenameCase) @@ -366,6 +446,7 @@ Promise.all([ // Add local variable include line to index.html (to access current SDK // version number). .then(addFirestoreTypeAliases) + .then(addDatabaseTypeAliases) .then(() => { fs.readFile(`${docPath}/index.html`, 'utf8').then(data => { // String to include devsite local variables. diff --git a/docgen/theme/layouts/default.hbs b/docgen/theme/layouts/default.hbs index 72111fb4e7..485d831d71 100644 --- a/docgen/theme/layouts/default.hbs +++ b/docgen/theme/layouts/default.hbs @@ -6,7 +6,9 @@ + + {{#ifCond model.name '==' project.name}}{{project.name}}{{else}}{{model.name}} | {{project.name}}{{/ifCond}} diff --git a/etc/firebase-admin.api.md b/etc/firebase-admin.api.md new file mode 100644 index 0000000000..427bc43099 --- /dev/null +++ b/etc/firebase-admin.api.md @@ -0,0 +1,1144 @@ +## API Report File for "firebase-admin" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Agent } from 'http'; +import { Bucket } from '@google-cloud/storage'; +import * as _firestore from '@google-cloud/firestore'; +import * as rtdb from '@firebase/database-types'; + +// @public (undocumented) +export function app(name?: string): app.App; + +// @public (undocumented) +export namespace app { + export interface App { + // (undocumented) + 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; + name: string; + options: AppOptions; + // (undocumented) + projectManagement(): projectManagement.ProjectManagement; + // (undocumented) + remoteConfig(): remoteConfig.RemoteConfig; + // (undocumented) + securityRules(): securityRules.SecurityRules; + // (undocumented) + storage(): storage.Storage; + } +} + +// @public +export function appCheck(app?: app.App): appCheck.AppCheck; + +// @public (undocumented) +export namespace appCheck { + export interface AppCheck { + // (undocumented) + app: app.App; + createToken(appId: string, options?: AppCheckTokenOptions): Promise; + verifyToken(appCheckToken: string): Promise; + } + export interface AppCheckToken { + token: string; + ttlMillis: number; + } + export interface AppCheckTokenOptions { + ttlMillis?: number; + } + export interface DecodedAppCheckToken { + // (undocumented) + [key: string]: any; + app_id: string; + aud: string[]; + exp: number; + iat: number; + iss: string; + sub: string; + } + export interface VerifyAppCheckTokenResponse { + appId: string; + token: appCheck.DecodedAppCheckToken; + } +} + +// @public +export interface AppOptions { + credential?: credential.Credential; + databaseAuthVariableOverride?: object | null; + databaseURL?: string; + httpAgent?: Agent; + projectId?: string; + serviceAccountId?: string; + storageBucket?: string; +} + +// @public (undocumented) +export const apps: (app.App | null)[]; + +// @public +export function auth(app?: app.App): auth.Auth; + +// @public (undocumented) +export namespace auth { + export interface ActionCodeSettings { + android?: { + packageName: string; + installApp?: boolean; + minimumVersion?: string; + }; + dynamicLinkDomain?: string; + handleCodeInApp?: boolean; + iOS?: { + bundleId: string; + }; + url: string; + } + // (undocumented) + export interface Auth extends BaseAuth { + // (undocumented) + app: app.App; + tenantManager(): TenantManager; + } + export type AuthFactorType = 'phone'; + export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; + export interface AuthProviderConfigFilter { + maxResults?: number; + pageToken?: string; + type: 'saml' | 'oidc'; + } + // (undocumented) + export interface BaseAuth { + createCustomToken(uid: string, developerClaims?: object): Promise; + createProviderConfig(config: AuthProviderConfig): Promise; + createSessionCookie(idToken: string, sessionCookieOptions: SessionCookieOptions): Promise; + createUser(properties: CreateRequest): Promise; + deleteProviderConfig(providerId: string): Promise; + deleteUser(uid: string): Promise; + deleteUsers(uids: string[]): Promise; + generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; + generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; + generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise; + getProviderConfig(providerId: string): Promise; + getUser(uid: string): Promise; + getUserByEmail(email: string): Promise; + getUserByPhoneNumber(phoneNumber: string): Promise; + 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; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifySessionCookie(sessionCookie: string, checkForRevocation?: boolean): Promise; + } + export interface BaseAuthProviderConfig { + displayName?: string; + enabled: boolean; + providerId: string; + } + export interface BaseCreateMultiFactorInfoRequest { + displayName?: string; + factorId: string; + } + export interface BaseUpdateMultiFactorInfoRequest { + displayName?: string; + enrollmentTime?: string; + factorId: string; + uid?: string; + } + export type CreateMultiFactorInfoRequest = CreatePhoneMultiFactorInfoRequest; + export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { + phoneNumber: string; + } + export interface CreateRequest extends UpdateRequest { + multiFactor?: MultiFactorCreateSettings; + uid?: string; + } + export type CreateTenantRequest = UpdateTenantRequest; + export interface DecodedIdToken { + // (undocumented) + [key: string]: any; + aud: string; + auth_time: number; + email?: string; + email_verified?: boolean; + exp: number; + firebase: { + identities: { + [key: string]: any; + }; + sign_in_provider: string; + sign_in_second_factor?: string; + second_factor_identifier?: string; + tenant?: string; + [key: string]: any; + }; + iat: number; + iss: string; + phone_number?: string; + picture?: string; + sub: string; + uid: string; + } + export interface DeleteUsersResult { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface EmailIdentifier { + // (undocumented) + email: string; + } + export interface EmailSignInProviderConfig { + enabled: boolean; + passwordRequired?: boolean; + } + export interface GetUsersResult { + notFound: UserIdentifier[]; + users: UserRecord[]; + } + // (undocumented) + export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; + export interface ListProviderConfigResults { + pageToken?: string; + providerConfigs: AuthProviderConfig[]; + } + export interface ListTenantsResult { + pageToken?: string; + tenants: Tenant[]; + } + export interface ListUsersResult { + pageToken?: string; + users: UserRecord[]; + } + export interface MultiFactorConfig { + factorIds?: AuthFactorType[]; + state: MultiFactorConfigState; + } + export type MultiFactorConfigState = 'ENABLED' | 'DISABLED'; + export interface MultiFactorCreateSettings { + enrolledFactors: CreateMultiFactorInfoRequest[]; + } + export interface MultiFactorInfo { + displayName?: string; + enrollmentTime?: string; + factorId: string; + toJSON(): object; + uid: string; + } + export interface MultiFactorSettings { + enrolledFactors: MultiFactorInfo[]; + toJSON(): object; + } + export interface MultiFactorUpdateSettings { + enrolledFactors: UpdateMultiFactorInfoRequest[] | null; + } + export interface OAuthResponseType { + code?: boolean; + idToken?: boolean; + } + export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { + clientId: string; + clientSecret?: string; + issuer: string; + responseType?: OAuthResponseType; + } + export interface OIDCUpdateAuthProviderRequest { + clientId?: string; + clientSecret?: string; + displayName?: string; + enabled?: boolean; + issuer?: string; + responseType?: OAuthResponseType; + } + export interface PhoneIdentifier { + // (undocumented) + phoneNumber: string; + } + export interface PhoneMultiFactorInfo extends MultiFactorInfo { + phoneNumber: string; + } + export interface ProviderIdentifier { + // (undocumented) + providerId: string; + // (undocumented) + providerUid: string; + } + export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { + callbackURL?: string; + idpEntityId: string; + rpEntityId: string; + ssoURL: string; + x509Certificates: string[]; + } + export interface SAMLUpdateAuthProviderRequest { + callbackURL?: string; + displayName?: string; + enabled?: boolean; + idpEntityId?: string; + rpEntityId?: string; + ssoURL?: string; + x509Certificates?: string[]; + } + export interface SessionCookieOptions { + expiresIn: number; + } + export interface Tenant { + anonymousSignInEnabled: boolean; + displayName?: string; + emailSignInConfig?: { + enabled: boolean; + passwordRequired?: boolean; + }; + multiFactorConfig?: MultiFactorConfig; + tenantId: string; + testPhoneNumbers?: { + [phoneNumber: string]: string; + }; + toJSON(): object; + } + export interface TenantAwareAuth extends BaseAuth { + tenantId: string; + } + export interface TenantManager { + // (undocumented) + authForTenant(tenantId: string): TenantAwareAuth; + createTenant(tenantOptions: CreateTenantRequest): Promise; + deleteTenant(tenantId: string): Promise; + getTenant(tenantId: string): Promise; + listTenants(maxResults?: number, pageToken?: string): Promise; + updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise; + } + export interface UidIdentifier { + // (undocumented) + uid: string; + } + // (undocumented) + export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; + export type UpdateMultiFactorInfoRequest = UpdatePhoneMultiFactorInfoRequest; + export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { + phoneNumber: string; + } + 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; + } + export interface UpdateTenantRequest { + anonymousSignInEnabled?: boolean; + displayName?: string; + emailSignInConfig?: EmailSignInProviderConfig; + multiFactorConfig?: MultiFactorConfig; + testPhoneNumbers?: { + [phoneNumber: string]: string; + } | null; + } + export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + export interface UserImportOptions { + hash: { + algorithm: HashAlgorithmType; + key?: Buffer; + saltSeparator?: Buffer; + rounds?: number; + memoryCost?: number; + parallelization?: number; + blockSize?: number; + derivedKeyLength?: number; + }; + } + export interface UserImportRecord { + customClaims?: { + [key: string]: any; + }; + disabled?: boolean; + displayName?: string; + email?: string; + emailVerified?: boolean; + metadata?: UserMetadataRequest; + multiFactor?: MultiFactorUpdateSettings; + passwordHash?: Buffer; + passwordSalt?: Buffer; + phoneNumber?: string; + photoURL?: string; + providerData?: UserProviderRequest[]; + tenantId?: string; + uid: string; + } + export interface UserImportResult { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface UserInfo { + displayName: string; + email: string; + phoneNumber: string; + photoURL: string; + providerId: string; + toJSON(): object; + uid: string; + } + export interface UserMetadata { + creationTime: string; + lastRefreshTime?: string | null; + lastSignInTime: string; + toJSON(): object; + } + export interface UserMetadataRequest { + creationTime?: string; + lastSignInTime?: string; + } + export interface UserProvider { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId?: string; + uid?: string; + } + export interface UserProviderRequest { + displayName?: string; + email?: string; + phoneNumber?: string; + photoURL?: string; + providerId: string; + uid: string; + } + export interface UserRecord { + customClaims?: { + [key: string]: any; + }; + disabled: boolean; + displayName?: string; + email?: string; + emailVerified: boolean; + metadata: UserMetadata; + multiFactor?: MultiFactorSettings; + passwordHash?: string; + passwordSalt?: string; + phoneNumber?: string; + photoURL?: string; + providerData: UserInfo[]; + tenantId?: string | null; + toJSON(): object; + tokensValidAfterTime?: string; + uid: string; + } +} + +// @public (undocumented) +export namespace credential { + export function applicationDefault(httpAgent?: Agent): Credential; + export function cert(serviceAccountPathOrObject: string | ServiceAccount, httpAgent?: Agent): Credential; + export interface Credential { + getAccessToken(): Promise; + } + export function refreshToken(refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential; +} + +// @public +export function database(app?: app.App): database.Database; + +// @public (undocumented) +export namespace database { + // (undocumented) + export interface Database extends rtdb.FirebaseDatabase { + getRules(): Promise; + getRulesJSON(): Promise; + setRules(source: string | Buffer | object): Promise; + } + import DataSnapshot = rtdb.DataSnapshot; + import EventType = rtdb.EventType; + import OnDisconnect = rtdb.OnDisconnect; + import Query = rtdb.Query; + import Reference = rtdb.Reference; + import ThenableReference = rtdb.ThenableReference; + import enableLogging = rtdb.enableLogging; + const ServerValue: rtdb.ServerValue; +} + +// @public +export interface FirebaseArrayIndexError { + error: FirebaseError; + index: number; +} + +// @public +export interface FirebaseError { + code: string; + message: string; + stack?: string; + toJSON(): object; +} + +// @public (undocumented) +export function firestore(app?: app.App): _firestore.Firestore; + +// @public (undocumented) +export namespace firestore { + import v1beta1 = _firestore.v1beta1; + import v1 = _firestore.v1; + import BulkWriter = _firestore.BulkWriter; + import BulkWriterOptions = _firestore.BulkWriterOptions; + import CollectionGroup = _firestore.CollectionGroup; + import CollectionReference = _firestore.CollectionReference; + import DocumentChangeType = _firestore.DocumentChangeType; + import DocumentData = _firestore.DocumentData; + import DocumentReference = _firestore.DocumentReference; + import DocumentSnapshot = _firestore.DocumentSnapshot; + import FieldPath = _firestore.FieldPath; + import FieldValue = _firestore.FieldValue; + import Firestore = _firestore.Firestore; + import FirestoreDataConverter = _firestore.FirestoreDataConverter; + import GeoPoint = _firestore.GeoPoint; + import GrpcStatus = _firestore.GrpcStatus; + import Precondition = _firestore.Precondition; + import Query = _firestore.Query; + import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; + import QueryPartition = _firestore.QueryPartition; + import QuerySnapshot = _firestore.QuerySnapshot; + import ReadOptions = _firestore.ReadOptions; + import Settings = _firestore.Settings; + import Timestamp = _firestore.Timestamp; + import Transaction = _firestore.Transaction; + import UpdateData = _firestore.UpdateData; + import WriteBatch = _firestore.WriteBatch; + import WriteResult = _firestore.WriteResult; + import setLogFunction = _firestore.setLogFunction; +} + +// @public +export interface GoogleOAuthAccessToken { + // (undocumented) + access_token: string; + // (undocumented) + expires_in: number; +} + +// @public (undocumented) +export function initializeApp(options?: AppOptions, name?: string): app.App; + +// @public +export function installations(app?: app.App): installations.Installations; + +// @public (undocumented) +export namespace installations { + export interface Installations { + // (undocumented) + app: app.App; + deleteInstallation(fid: string): Promise; + } +} + +// @public @deprecated +export function instanceId(app?: app.App): instanceId.InstanceId; + +// @public (undocumented) +export namespace instanceId { + // @deprecated + export interface InstanceId { + // (undocumented) + app: app.App; + // @deprecated + deleteInstanceId(instanceId: string): Promise; + } +} + +// @public +export function machineLearning(app?: app.App): machineLearning.MachineLearning; + +// @public (undocumented) +export namespace machineLearning { + // (undocumented) + export interface AutoMLTfliteModelOptions extends ModelOptionsBase { + // (undocumented) + tfliteModel: { + automlModel: string; + }; + } + // (undocumented) + export interface GcsTfliteModelOptions extends ModelOptionsBase { + // (undocumented) + tfliteModel: { + gcsTfliteUri: string; + }; + } + export interface ListModelsOptions { + filter?: string; + pageSize?: number; + pageToken?: string; + } + export interface ListModelsResult { + readonly models: Model[]; + readonly pageToken?: string; + } + export interface MachineLearning { + app: app.App; + createModel(model: ModelOptions): Promise; + deleteModel(modelId: string): Promise; + getModel(modelId: string): Promise; + listModels(options?: ListModelsOptions): Promise; + publishModel(modelId: string): Promise; + unpublishModel(modelId: string): Promise; + updateModel(modelId: string, model: ModelOptions): Promise; + } + export interface Model { + readonly createTime: string; + readonly displayName: string; + readonly etag: string; + readonly locked: boolean; + readonly modelHash?: string; + readonly modelId: string; + readonly published: boolean; + readonly tags?: string[]; + readonly tfliteModel?: TFLiteModel; + toJSON(): { + [key: string]: any; + }; + readonly updateTime: string; + readonly validationError?: string; + waitForUnlocked(maxTimeMillis?: number): Promise; + } + // (undocumented) + export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; + export interface ModelOptionsBase { + // (undocumented) + displayName?: string; + // (undocumented) + tags?: string[]; + } + export interface TFLiteModel { + readonly automlModel?: string; + readonly gcsTfliteUri?: string; + readonly sizeBytes: number; + } +} + +// @public +export function messaging(app?: app.App): messaging.Messaging; + +// @public (undocumented) +export namespace messaging { + export interface AndroidConfig { + collapseKey?: string; + data?: { + [key: string]: string; + }; + fcmOptions?: AndroidFcmOptions; + notification?: AndroidNotification; + priority?: ('high' | 'normal'); + restrictedPackageName?: string; + ttl?: number; + } + export interface AndroidFcmOptions { + analyticsLabel?: string; + } + export interface AndroidNotification { + body?: string; + bodyLocArgs?: string[]; + bodyLocKey?: string; + channelId?: string; + clickAction?: string; + color?: string; + defaultLightSettings?: boolean; + defaultSound?: boolean; + defaultVibrateTimings?: boolean; + eventTimestamp?: Date; + icon?: string; + imageUrl?: string; + lightSettings?: LightSettings; + localOnly?: boolean; + notificationCount?: number; + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + sound?: string; + sticky?: boolean; + tag?: string; + ticker?: string; + title?: string; + titleLocArgs?: string[]; + titleLocKey?: string; + vibrateTimingsMillis?: number[]; + visibility?: ('private' | 'public' | 'secret'); + } + export interface ApnsConfig { + fcmOptions?: ApnsFcmOptions; + headers?: { + [key: string]: string; + }; + payload?: ApnsPayload; + } + export interface ApnsFcmOptions { + analyticsLabel?: string; + imageUrl?: string; + } + export interface ApnsPayload { + // (undocumented) + [customData: string]: any; + aps: Aps; + } + export interface Aps { + // (undocumented) + [customData: string]: any; + alert?: string | ApsAlert; + badge?: number; + category?: string; + contentAvailable?: boolean; + mutableContent?: boolean; + sound?: string | CriticalSound; + threadId?: string; + } + // (undocumented) + export interface ApsAlert { + // (undocumented) + actionLocKey?: string; + // (undocumented) + body?: string; + // (undocumented) + launchImage?: string; + // (undocumented) + locArgs?: string[]; + // (undocumented) + locKey?: string; + // (undocumented) + subtitle?: string; + // (undocumented) + subtitleLocArgs?: string[]; + // (undocumented) + subtitleLocKey?: string; + // (undocumented) + title?: string; + // (undocumented) + titleLocArgs?: string[]; + // (undocumented) + titleLocKey?: string; + } + // (undocumented) + export interface BaseMessage { + // (undocumented) + android?: AndroidConfig; + // (undocumented) + apns?: ApnsConfig; + // (undocumented) + data?: { + [key: string]: string; + }; + // (undocumented) + fcmOptions?: FcmOptions; + // (undocumented) + notification?: Notification; + // (undocumented) + webpush?: WebpushConfig; + } + export interface BatchResponse { + failureCount: number; + responses: SendResponse[]; + successCount: number; + } + // (undocumented) + export interface ConditionMessage extends BaseMessage { + // (undocumented) + condition: string; + } + export interface CriticalSound { + critical?: boolean; + name: string; + volume?: number; + } + export interface DataMessagePayload { + // (undocumented) + [key: string]: string; + } + export interface FcmOptions { + analyticsLabel?: string; + } + export interface LightSettings { + color: string; + lightOffDurationMillis: number; + lightOnDurationMillis: number; + } + export type Message = TokenMessage | TopicMessage | ConditionMessage; + // (undocumented) + export interface Messaging { + app: app.App; + send(message: Message, dryRun?: boolean): Promise; + sendAll(messages: Array, dryRun?: boolean): Promise; + sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise; + sendToCondition(condition: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToDevice(registrationToken: string | string[], payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToDeviceGroup(notificationKey: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + sendToTopic(topic: string, payload: MessagingPayload, options?: MessagingOptions): Promise; + subscribeToTopic(registrationTokens: string | string[], topic: string): Promise; + unsubscribeFromTopic(registrationTokens: string | string[], topic: string): Promise; + } + export interface MessagingConditionResponse { + messageId: number; + } + export interface MessagingDeviceGroupResponse { + failedRegistrationTokens: string[]; + failureCount: number; + successCount: number; + } + // (undocumented) + export interface MessagingDeviceResult { + canonicalRegistrationToken?: string; + error?: FirebaseError; + messageId?: string; + } + export interface MessagingDevicesResponse { + // (undocumented) + canonicalRegistrationTokenCount: number; + // (undocumented) + failureCount: number; + // (undocumented) + multicastId: number; + // (undocumented) + results: MessagingDeviceResult[]; + // (undocumented) + successCount: number; + } + export interface MessagingOptions { + // (undocumented) + [key: string]: any | undefined; + collapseKey?: string; + contentAvailable?: boolean; + dryRun?: boolean; + mutableContent?: boolean; + priority?: string; + restrictedPackageName?: string; + timeToLive?: number; + } + export interface MessagingPayload { + data?: DataMessagePayload; + notification?: NotificationMessagePayload; + } + export interface MessagingTopicManagementResponse { + errors: FirebaseArrayIndexError[]; + failureCount: number; + successCount: number; + } + export interface MessagingTopicResponse { + messageId: number; + } + export interface MulticastMessage extends BaseMessage { + // (undocumented) + tokens: string[]; + } + export interface Notification { + body?: string; + imageUrl?: string; + title?: string; + } + export interface NotificationMessagePayload { + // (undocumented) + [key: string]: string | undefined; + badge?: string; + body?: string; + bodyLocArgs?: string; + bodyLocKey?: string; + clickAction?: string; + color?: string; + icon?: string; + sound?: string; + tag?: string; + title?: string; + titleLocArgs?: string; + titleLocKey?: string; + } + export interface SendResponse { + error?: FirebaseError; + messageId?: string; + success: boolean; + } + // (undocumented) + export interface TokenMessage extends BaseMessage { + // (undocumented) + token: string; + } + // (undocumented) + export interface TopicMessage extends BaseMessage { + // (undocumented) + topic: string; + } + export interface WebpushConfig { + data?: { + [key: string]: string; + }; + fcmOptions?: WebpushFcmOptions; + headers?: { + [key: string]: string; + }; + notification?: WebpushNotification; + } + export interface WebpushFcmOptions { + link?: string; + } + export interface WebpushNotification { + // (undocumented) + [key: string]: any; + actions?: Array<{ + action: string; + icon?: string; + title: string; + }>; + badge?: string; + body?: string; + data?: any; + dir?: 'auto' | 'ltr' | 'rtl'; + icon?: string; + image?: string; + lang?: string; + renotify?: boolean; + requireInteraction?: boolean; + silent?: boolean; + tag?: string; + timestamp?: number; + title?: string; + vibrate?: number | number[]; + } + {}; +} + +// @public +export function projectManagement(app?: app.App): projectManagement.ProjectManagement; + +// @public (undocumented) +export namespace projectManagement { + export interface AndroidApp { + addShaCertificate(certificateToAdd: ShaCertificate): Promise; + // (undocumented) + appId: string; + deleteShaCertificate(certificateToRemove: ShaCertificate): Promise; + getConfig(): Promise; + getMetadata(): Promise; + getShaCertificates(): Promise; + setDisplayName(newDisplayName: string): Promise; + } + export interface AndroidAppMetadata extends AppMetadata { + packageName: string; + // (undocumented) + platform: AppPlatform.ANDROID; + } + export interface AppMetadata { + appId: string; + displayName?: string; + platform: AppPlatform; + projectId: string; + resourceName: string; + } + export enum AppPlatform { + ANDROID = "ANDROID", + IOS = "IOS", + PLATFORM_UNKNOWN = "PLATFORM_UNKNOWN" + } + export interface IosApp { + // (undocumented) + appId: string; + getConfig(): Promise; + getMetadata(): Promise; + setDisplayName(newDisplayName: string): Promise; + } + export interface IosAppMetadata extends AppMetadata { + bundleId: string; + // (undocumented) + platform: AppPlatform.IOS; + } + export interface ProjectManagement { + androidApp(appId: string): AndroidApp; + // (undocumented) + app: app.App; + createAndroidApp(packageName: string, displayName?: string): Promise; + createIosApp(bundleId: string, displayName?: string): Promise; + iosApp(appId: string): IosApp; + listAndroidApps(): Promise; + listAppMetadata(): Promise; + listIosApps(): Promise; + setDisplayName(newDisplayName: string): Promise; + shaCertificate(shaHash: string): ShaCertificate; + } + export interface ShaCertificate { + certType: ('sha1' | 'sha256'); + resourceName?: string; + shaHash: string; + } +} + +// @public +export function remoteConfig(app?: app.App): remoteConfig.RemoteConfig; + +// @public (undocumented) +export namespace remoteConfig { + export interface ExplicitParameterValue { + value: string; + } + export interface InAppDefaultValue { + useInAppDefault: boolean; + } + export interface ListVersionsOptions { + endTime?: Date | string; + endVersionNumber?: string | number; + pageSize?: number; + pageToken?: string; + startTime?: Date | string; + } + export interface ListVersionsResult { + nextPageToken?: string; + versions: Version[]; + } + export interface RemoteConfig { + // (undocumented) + app: app.App; + createTemplateFromJSON(json: string): RemoteConfigTemplate; + getTemplate(): Promise; + getTemplateAtVersion(versionNumber: number | string): Promise; + listVersions(options?: ListVersionsOptions): Promise; + publishTemplate(template: RemoteConfigTemplate, options?: { + force: boolean; + }): Promise; + rollback(versionNumber: string | number): Promise; + validateTemplate(template: RemoteConfigTemplate): Promise; + } + export interface RemoteConfigCondition { + expression: string; + name: string; + tagColor?: TagColor; + } + export interface RemoteConfigParameter { + conditionalValues?: { + [key: string]: RemoteConfigParameterValue; + }; + defaultValue?: RemoteConfigParameterValue; + description?: string; + } + export interface RemoteConfigParameterGroup { + description?: string; + parameters: { + [key: string]: RemoteConfigParameter; + }; + } + export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + export interface RemoteConfigTemplate { + conditions: RemoteConfigCondition[]; + readonly etag: string; + parameterGroups: { + [key: string]: RemoteConfigParameterGroup; + }; + parameters: { + [key: string]: RemoteConfigParameter; + }; + version?: Version; + } + export interface RemoteConfigUser { + email: string; + imageUrl?: string; + name?: string; + } + export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + export interface Version { + description?: string; + isLegacy?: boolean; + rollbackSource?: string; + updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | 'REST_API' | 'ADMIN_SDK_NODE'); + updateTime?: string; + updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + updateUser?: RemoteConfigUser; + versionNumber?: string; + } +} + +// @public (undocumented) +export const SDK_VERSION: string; + +// @public +export function securityRules(app?: app.App): securityRules.SecurityRules; + +// @public (undocumented) +export namespace securityRules { + export interface Ruleset extends RulesetMetadata { + // (undocumented) + readonly source: RulesFile[]; + } + export interface RulesetMetadata { + readonly createTime: string; + readonly name: string; + } + export interface RulesetMetadataList { + readonly nextPageToken?: string; + readonly rulesets: RulesetMetadata[]; + } + export interface RulesFile { + // (undocumented) + readonly content: string; + // (undocumented) + readonly name: string; + } + export interface SecurityRules { + // (undocumented) + app: app.App; + createRuleset(file: RulesFile): Promise; + createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; + deleteRuleset(name: string): Promise; + getFirestoreRuleset(): Promise; + getRuleset(name: string): Promise; + getStorageRuleset(bucket?: string): Promise; + listRulesetMetadata(pageSize?: number, nextPageToken?: string): Promise; + releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; + releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; + releaseStorageRuleset(ruleset: string | RulesetMetadata, bucket?: string): Promise; + releaseStorageRulesetFromSource(source: string | Buffer, bucket?: string): Promise; + } +} + +// @public (undocumented) +export interface ServiceAccount { + // (undocumented) + clientEmail?: string; + // (undocumented) + privateKey?: string; + // (undocumented) + projectId?: string; +} + +// @public +export function storage(app?: app.App): storage.Storage; + +// @public (undocumented) +export namespace storage { + export interface Storage { + app: app.App; + // (undocumented) + bucket(name?: string): Bucket; + } +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/gulpfile.js b/gulpfile.js index 959cf46c05..9ca1aeb941 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,17 +20,15 @@ /**************/ /* REQUIRES */ /**************/ -var fs = require('fs'); var _ = require('lodash'); var gulp = require('gulp'); var pkg = require('./package.json'); // File I/O -var fs = require('fs'); var ts = require('gulp-typescript'); var del = require('del'); var header = require('gulp-header'); -var replace = require('gulp-replace'); +var filter = require('gulp-filter'); /****************/ @@ -45,17 +44,15 @@ var paths = { '!test/integration/typescript/src/example*.ts', ], - databaseSrc: [ - 'src/**/*.js' - ], - 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 }); var buildTest = ts.createProject('tsconfig.json'); @@ -71,19 +68,31 @@ 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/**/index.d.ts', + 'lib/firebase-namespace-api.d.ts', + '!lib/utils/index.d.ts', + ]; + + workflow = workflow.pipe(filter(configuration)); + + // Write to build directory + return workflow.pipe(gulp.dest(paths.build)) }); /** @@ -96,34 +105,22 @@ gulp.task('compile_test', function() { .pipe(buildTest()) }); -gulp.task('copyDatabase', function() { - return gulp.src(paths.databaseSrc) - // Add headers - .pipe(header(fs.readFileSync('third_party/database-license.txt', 'utf8'))) - .pipe(header(banner)) - - // Write to build directory - .pipe(gulp.dest(paths.build)) -}); - gulp.task('copyTypings', function() { - return gulp.src('src/*.d.ts') + return gulp.src(['src/index.d.ts', 'src/firebase-namespace.d.ts']) // Add header .pipe(header(banner)) - - // Write to build directory .pipe(gulp.dest(paths.build)) }); -gulp.task('compile_all', gulp.series('compile', 'copyDatabase', 'copyTypings', 'compile_test')); +gulp.task('compile_all', gulp.series('compile', 'copyTypings', 'compile_test')); // Regenerates js every time a source file changes gulp.task('watch', function() { - gulp.watch(paths.src.concat(paths.test), {ignoreInitial: false}, gulp.series('compile_all')); + gulp.watch(paths.src.concat(paths.test), { ignoreInitial: false }, gulp.series('compile_all')); }); // Build task -gulp.task('build', gulp.series('cleanup', 'compile', 'copyDatabase', 'copyTypings')); +gulp.task('build', gulp.series('cleanup', 'compile', 'copyTypings')); // Default task gulp.task('default', gulp.series('build')); diff --git a/package-lock.json b/package-lock.json index ff44fd069a..d9016d918c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,68 +1,259 @@ { "name": "firebase-admin", - "version": "8.12.1", + "version": "9.10.0", "lockfileVersion": 1, "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/compat-data": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz", + "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==", + "dev": true + }, + "@babel/core": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.3.tgz", + "integrity": "sha512-jB5AmTKOCSJIZ72sd78ECEhuPiDMKlQdDI/4QRI6lzYATx5SSogS1oQA2AoPecRCknm30gHi2l+QVvNUu3wZAg==", "dev": true, "requires": { - "@babel/highlight": "^7.0.0" + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.3", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.2", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.3", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.3.tgz", + "integrity": "sha512-bn0S6flG/j0xtQdz3hsjJ624h3W0r3llttBMfyHX3YrZ/KtLYr15bjA0FXkgW7FpvrDuTuElXeVjiKlYRpnOFA==", "dev": true, "requires": { - "@babel/types": "^7.4.4", + "@babel/types": "^7.14.2", "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" + "source-map": "^0.5.0" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.13.16", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz", + "integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.15", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.14.5", + "semver": "^6.3.0" } }, "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.2.tgz", + "integrity": "sha512-NYZlkZRydxw+YT56IlhIcS8PAhb+FEUiOzuhFTfqDyPmzAhRge6ua0dQYT/Uh0t/EDHq05/i+e5M2d4XvjgarQ==", "dev": true, "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.14.2" } }, "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", "dev": true, "requires": { - "@babel/types": "^7.0.0" + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.2.tgz", + "integrity": "sha512-OznJUda/soKXv0XhpvzGWDnml4Qnwp16GN+D/kZIdLsWoHj05kyu8Rm5kXmMef+rVJZ0+4pSGLkeixdqNUATDA==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + } + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-replace-supers": { + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.14.3.tgz", + "integrity": "sha512-Rlh8qEWZSTfdz+tgNV/N4gz1a0TMNwCUcENhMjHTHKp3LseYH5Jha0NSlyTQWMnjbYcwFt+bqAMqSLHVXkQ6UA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.14.2", + "@babel/types": "^7.14.2" + } + }, + "@babel/helper-simple-access": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" } }, "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", + "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", "dev": true, "requires": { - "@babel/types": "^7.4.4" + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" } }, "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", "dev": true, "requires": { + "@babel/helper-validator-identifier": "^7.10.4", "chalk": "^2.0.0", - "esutils": "^2.0.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -98,452 +289,778 @@ } }, "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", + "version": "7.14.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.3.tgz", + "integrity": "sha512-7MpZDIfI7sUC5zWo2+foJ50CSI5lcqDehZ0lVgIhSi4bFEk94fLAKlF3Q0nzSQQ+ca0lm+O6G9ztKVBeu8PMRQ==", "dev": true }, "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/traverse": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", - "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.2.tgz", + "integrity": "sha512-TsdRgvBFHMyHOOzcP9S6QU0QQtjxlRpEYOy3mcCO5RgmC305ki42aSAmfZEMSSYBla2oZ9BMqYlncBaKmD/7iA==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.5", - "@babel/types": "^7.4.4", + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.2", + "@babel/helper-function-name": "^7.14.2", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.2", + "@babel/types": "^7.14.2", "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" + "globals": "^11.1.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dev": true, "requires": { - "ms": "^2.1.1" + "@babel/highlight": "^7.12.13" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", + "version": "7.14.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.2.tgz", + "integrity": "sha512-SdjAG/3DikRHpUOjxZgnkbR11xUlyDMUFJdvnIgZEE16mqmY0BINMmc4//JMJglEmn6i7sq6p+mGrFWyZ98EEw==", "dev": true, "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", + "@babel/helper-validator-identifier": "^7.14.0", "to-fast-properties": "^2.0.0" + }, + "dependencies": { + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + } } }, "@firebase/app": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.2.tgz", - "integrity": "sha512-rAxc90+82GAPpUxS02EO0dys4+TeQ6XjFjCwQz/OVptGeLgxN9ZoXYAf/bxyeYOdLxJW0kbEKE/0xXaJDt5gsg==", + "version": "0.6.26", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.6.26.tgz", + "integrity": "sha512-y4tpb+uiYLQC5+/AHBtIGZMaTjJ2BHQEsXmPqxyhfVFDzWMcXFsc//RVxA/0OejajhJR6GeqDcIS3m47mUD+Aw==", "dev": true, "requires": { - "@firebase/app-types": "0.6.0", - "@firebase/component": "0.1.10", - "@firebase/logger": "0.2.2", - "@firebase/util": "0.2.45", + "@firebase/app-types": "0.6.2", + "@firebase/component": "0.5.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "1.1.0", "dom-storage": "2.1.0", - "tslib": "1.11.1", + "tslib": "^2.1.0", "xmlhttprequest": "1.8.0" }, "dependencies": { "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", "dev": true } } }, "@firebase/app-types": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.0.tgz", - "integrity": "sha512-ld6rzjXk/SUauHiQZJkeuSJpxIZ5wdnWuF5fWBFQNPaxsaJ9kyYg9GqEvwZ1z2e6JP5cU9gwRBlfW1WkGtGDYA==" + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.6.2.tgz", + "integrity": "sha512-2VXvq/K+n8XMdM4L2xy5bYp2ZXMawJXluUIDzUBvMthVR+lhxK4pfFiqr1mmDbv9ydXvEAuFsD+6DpcZuJcSSw==" }, "@firebase/auth": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.13.3.tgz", - "integrity": "sha512-Ks+6PdLzuxrlkbnSbrMKpOdCbvrfJEBwXe2/GfHCDuJWsxUEx2qFcda+g04pgXnlf1qCjPeNEJM8U0WzTvGHyA==", + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.16.5.tgz", + "integrity": "sha512-Cgs/TlVot2QkbJyEphvKmu+2qxYlNN+Q2+29aqZwryrnn1eLwlC7nT89K6O91/744HJRtiThm02bMj2Wh61E3Q==", "dev": true, "requires": { - "@firebase/auth-types": "0.9.3" + "@firebase/auth-types": "0.10.3" } }, "@firebase/auth-interop-types": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.4.tgz", - "integrity": "sha512-CLKNS84KGAv5lRnHTQZFWoR11Ti7gIPFirDDXWek/fSU+TdYdnxJFR5XSD4OuGyzUYQ3Dq7aVj5teiRdyBl9hA==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz", + "integrity": "sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==" }, "@firebase/auth-types": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.9.3.tgz", - "integrity": "sha512-eS9BEuZ1XxBQReUhG6lbus9ScOgHwqYPT7a645PKa/tBb1BWsgivwRFzH0BATPGLP+JTtRvy5JqEsQ25S7J4ig==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.10.3.tgz", + "integrity": "sha512-zExrThRqyqGUbXOFrH/sowuh2rRtfKHp9SBVY2vOqKWdCX1Ztn682n9WLtlUDsiYVIbBcwautYWk2HyCGFv0OA==", "dev": true }, "@firebase/component": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.1.10.tgz", - "integrity": "sha512-Iy1+f8wp6mROz19oxWUd31NxMlGxtW1IInGHITnVa6eZtXOg0lxcbgYeLp9W3PKzvvNfshHU0obDkcMY97zRAw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.2.tgz", + "integrity": "sha512-QT+o6VaBCz/k8wmC/DErU9dQK2QeIoHtkBkryZVTSRkrvulglEWNIpbPp86UbuqZZd1wwzoh6m7BL6JbdEp9SQ==", + "dev": true, "requires": { - "@firebase/util": "0.2.45", - "tslib": "1.11.1" + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" }, "dependencies": { - "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", - "requires": { - "tslib": "1.11.1" - } - }, "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "dev": true } } }, "@firebase/database": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.6.1.tgz", - "integrity": "sha512-7XqUbj3nK2vEdFjGOXBfKISmpLrM0caIwwfDPxhn6i7X/g6AIH+D1limH+Jit4QeKMh/IJZDNqO7P+Fz+e8q1Q==", - "requires": { - "@firebase/auth-interop-types": "0.1.4", - "@firebase/component": "0.1.10", - "@firebase/database-types": "0.5.0", - "@firebase/logger": "0.2.2", - "@firebase/util": "0.2.45", + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.10.6.tgz", + "integrity": "sha512-AGxRnKaJQd4Pq7sblrWI39XM5N2u/pZOeopMxVRja38Cubxp6P5T7lzpp0xNSOQ/RszAoHskGIlCfIz+teaXSQ==", + "requires": { + "@firebase/auth-interop-types": "0.1.6", + "@firebase/component": "0.5.4", + "@firebase/database-types": "0.7.2", + "@firebase/logger": "0.2.6", + "@firebase/util": "1.1.0", "faye-websocket": "0.11.3", - "tslib": "1.11.1" + "tslib": "^2.1.0" }, "dependencies": { - "@firebase/logger": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.2.tgz", - "integrity": "sha512-MbEy17Ha1w/DlLtvxG89ScQ+0+yoElGKJ1nUCQHHLjeMNsRwd2wnUPOVCsZvtBzQp8Z0GaFmD4a2iG2v91lEbA==" - }, - "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", + "@firebase/component": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.4.tgz", + "integrity": "sha512-KoLDPTsvxWr6FT9kn/snffJItaWXZLHLJlZVKiiw+flKE6MVA8Eec+ctvM2zcsMZzC2Z47gFnVqywfBlOevmpQ==", "requires": { - "tslib": "1.11.1" + "@firebase/util": "1.1.0", + "tslib": "^2.1.0" } }, "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } }, "@firebase/database-types": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.5.0.tgz", - "integrity": "sha512-6/W3frFznYOALtw2nrWVPK2ytgdl89CzTqVBHCCGf22wT6uKU63iDBo+Nw+7olFGpD15O0zwYalFIcMZ27tkew==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.7.2.tgz", + "integrity": "sha512-cdAd/dgwvC0r3oLEDUR+ULs1vBsEvy0b27nlzKhU6LQgm9fCDzgaH9nFGv8x+S9dly4B0egAXkONkVoWcOAisg==", "requires": { - "@firebase/app-types": "0.6.0" + "@firebase/app-types": "0.6.2" } }, "@firebase/logger": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.2.tgz", - "integrity": "sha512-MbEy17Ha1w/DlLtvxG89ScQ+0+yoElGKJ1nUCQHHLjeMNsRwd2wnUPOVCsZvtBzQp8Z0GaFmD4a2iG2v91lEbA==", - "dev": true + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.2.6.tgz", + "integrity": "sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw==" }, "@firebase/util": { - "version": "0.2.45", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-0.2.45.tgz", - "integrity": "sha512-k3IqXaIgwlPg7m5lXmMUtkqA/p+LMFkFQIqBuDtdT0iyWB6kQDokyjw2Sgd3GoTybs6tWqUKFZupZpV6r73UHw==", - "dev": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.1.0.tgz", + "integrity": "sha512-lfuSASuPKNdfebuFR8rjFamMQUPH9iiZHcKS755Rkm/5gRT0qC7BMhCh3ZkHf7NVbplzIc/GhmX2jM+igDRCag==", "requires": { - "tslib": "1.11.1" + "tslib": "^2.1.0" }, "dependencies": { "tslib": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", - "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", - "dev": true + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } }, "@google-cloud/common": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-2.2.3.tgz", - "integrity": "sha512-lvw54mGKn8VqVIy2NzAk0l5fntBFX4UwQhHk6HaqkyCQ7WBl5oz4XhzKMtMilozF/3ObPcDogqwuyEWyZ6rnQQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-3.3.3.tgz", + "integrity": "sha512-2PwPDE47N4WiWQK/F35vE5aWVoCjKQ2NW8r8OFAg6QslkLMjX6WNcmUO8suYlSkavc58qOvzA4jG6eVkC90i8Q==", "optional": true, "requires": { - "@google-cloud/projectify": "^1.0.0", - "@google-cloud/promisify": "^1.0.0", - "arrify": "^2.0.0", - "duplexify": "^3.6.0", + "@google-cloud/projectify": "^2.0.0", + "@google-cloud/promisify": "^2.0.0", + "arrify": "^2.0.1", + "duplexify": "^4.1.1", "ent": "^2.2.0", "extend": "^3.0.2", - "google-auth-library": "^5.5.0", - "retry-request": "^4.0.0", - "teeny-request": "^5.2.1" + "google-auth-library": "^6.0.0", + "retry-request": "^4.1.1", + "teeny-request": "^7.0.0" }, "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } } } }, "@google-cloud/firestore": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-3.0.0.tgz", - "integrity": "sha512-Os6rXW6z9bd2sVdjDJRUneF5u7keH+vpWX/Uddq0dlFyNbwBSgCBFWt+0VYXkgQE+O8B8i1p+FdaleTjFFuRVA==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.13.1.tgz", + "integrity": "sha512-LtxboFZQ3MGwy1do8a0ykMJocM+TFgOpZoAihMwW498UDd641DJgJu0Kw0CD0bPpEaYUfhbeAUBq2ZO63DOz7g==", "optional": true, "requires": { - "bun": "^0.0.12", - "deep-equal": "^1.1.1", + "fast-deep-equal": "^3.1.1", "functional-red-black-tree": "^1.0.1", - "google-gax": "^1.12.0", - "through2": "^3.0.0" - }, - "dependencies": { - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "optional": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true - } + "google-gax": "^2.17.0", + "protobufjs": "^6.8.6" } }, "@google-cloud/paginator": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-2.0.1.tgz", - "integrity": "sha512-HZ6UTGY/gHGNriD7OCikYWL/Eu0sTEur2qqse2w6OVsz+57se3nTkqH14JIPxtf0vlEJ8IJN5w3BdZ22pjCB8g==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz", + "integrity": "sha512-N4Uk4BT1YuskfRhKXBs0n9Lg2YTROZc6IMpkO/8DIHODtm5s3xY8K5vVBo23v/2XulY3azwITQlYWgT4GdLsUw==", "optional": true, "requires": { "arrify": "^2.0.0", "extend": "^3.0.2" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - } } }, "@google-cloud/projectify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-1.0.1.tgz", - "integrity": "sha512-xknDOmsMgOYHksKc1GPbwDLsdej8aRNIA17SlSZgQdyrcC0lx0OGo4VZgYfwoEU1YS8oUxF9Y+6EzDOb0eB7Xg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-2.0.1.tgz", + "integrity": "sha512-ZDG38U/Yy6Zr21LaR3BTiiLtpJl6RkPS/JwoRT453G+6Q1DhlV0waNf8Lfu+YVYGIIxgKnLayJRfYlFJfiI8iQ==", "optional": true }, "@google-cloud/promisify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-1.0.2.tgz", - "integrity": "sha512-7WfV4R/3YV5T30WRZW0lqmvZy9hE2/p9MvpI34WuKa2Wz62mLu5XplGTFEMK6uTbJCLWUxTcZ4J4IyClKucE5g==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-2.0.3.tgz", + "integrity": "sha512-d4VSA86eL/AFTe5xtyZX+ePUjE8dIFu2T8zmdeNBSa5/kNgXPCx/o/wbFNHAGLJdGnk1vddRuMESD9HbOC8irw==", + "optional": true }, "@google-cloud/storage": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-4.1.2.tgz", - "integrity": "sha512-kYP7h2SMx5KmbIbeQ4qHoBm9uYFRZOR96BCYpzGWYO8ii157sA1nmmULai0lcrVwOhfVXoReZUpflTc5lFN80g==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.3.0.tgz", + "integrity": "sha512-3t5UF3SZ14Bw2kcBHubCai6EIugU2GnQOstYWVSFuoO8IJ94RAaIOPq/dtexvQbUTpBTAGpd5smVR9WPL1mJVw==", "optional": true, "requires": { - "@google-cloud/common": "^2.1.1", - "@google-cloud/paginator": "^2.0.0", - "@google-cloud/promisify": "^1.0.0", + "@google-cloud/common": "^3.3.0", + "@google-cloud/paginator": "^3.0.0", + "@google-cloud/promisify": "^2.0.0", "arrify": "^2.0.0", "compressible": "^2.0.12", "concat-stream": "^2.0.0", - "date-and-time": "^0.10.0", + "date-and-time": "^0.14.0", "duplexify": "^3.5.0", "extend": "^3.0.2", - "gaxios": "^2.0.1", - "gcs-resumable-upload": "^2.2.4", + "gaxios": "^3.0.0", + "gcs-resumable-upload": "^3.1.0", "hash-stream-validation": "^0.2.2", "mime": "^2.2.0", "mime-types": "^2.0.8", "onetime": "^5.1.0", - "p-limit": "^2.2.0", + "p-limit": "^3.0.1", "pumpify": "^2.0.0", - "readable-stream": "^3.4.0", "snakeize": "^0.1.0", "stream-events": "^1.0.1", - "through2": "^3.0.0", "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", - "optional": true - }, - "concat-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", - "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + } + }, + "@grpc/grpc-js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.3.4.tgz", + "integrity": "sha512-AxtZcm0mArQhY9z8T3TynCYVEaSKxNCa9mVhVwBCUnsuUEe8Zn94bPYYKVQSLt+hJJ1y0ukr3mUvtWfcATL/IQ==", + "optional": true, + "requires": { + "@types/node": ">=12.12.47" + } + }, + "@grpc/proto-loader": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", + "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", + "optional": true, + "requires": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^6.10.0", + "yargs": "^16.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "optional": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "optional": true, "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.0.2", - "typedarray": "^0.0.6" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, - "pump": { + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "optional": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "optional": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "optional": true, "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - } + "ansi-regex": "^5.0.0" } }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "optional": true, "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "optional": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "optional": true, + "requires": { + "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" + } + }, + "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==", + "optional": 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": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, - "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==", + "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": { - "safe-buffer": "~5.2.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "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 } } }, - "@grpc/grpc-js": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-0.6.14.tgz", - "integrity": "sha512-M6q3MtHzk0NQPs1PB+SXSJtkDtK8WXJh+1B1WVJQp5HTURadzj9t1bUb/Fjhq+K57lKsOgL60r8WGmE7vks1eg==", - "optional": true, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@mapbox/node-pre-gyp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz", + "integrity": "sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA==", + "dev": true, "requires": { - "semver": "^6.2.0" + "detect-libc": "^1.0.3", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.1", + "nopt": "^5.0.0", + "npmlog": "^4.1.2", + "rimraf": "^3.0.2", + "semver": "^7.3.4", + "tar": "^6.1.0" }, "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, - "@grpc/proto-loader": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.5.3.tgz", - "integrity": "sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ==", - "optional": true, + "@microsoft/api-extractor": { + "version": "7.15.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.15.2.tgz", + "integrity": "sha512-/Y/n+QOc1vM6Vg3OAUByT/wXdZciE7jV3ay33+vxl3aKva5cNsuOauL14T7XQWUiLko3ilPwrcnFcEjzXpLsuA==", + "dev": true, "requires": { - "lodash.camelcase": "^4.3.0", - "protobufjs": "^6.8.6" + "@microsoft/api-extractor-model": "7.13.2", + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.38.0", + "@rushstack/rig-package": "0.2.12", + "@rushstack/ts-command-line": "4.7.10", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "source-map": "~0.6.1", + "typescript": "~4.2.4" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "typescript": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@microsoft/api-extractor-model": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.2.tgz", + "integrity": "sha512-gA9Q8q5TPM2YYk7rLinAv9KqcodrmRC13BVmNzLswjtFxpz13lRh0BmrqD01/sddGpGMIuWFYlfUM4VSWxnggA==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.38.0" + } + }, + "@microsoft/tsdoc": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz", + "integrity": "sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.13.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "requires": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + } + } + } + }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" } }, + "@panva/asn1.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz", + "integrity": "sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==" + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", + "optional": true }, "@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true }, "@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true }, "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", + "optional": true }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -552,63 +1069,150 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", + "optional": true }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", + "optional": true }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", + "optional": true }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", + "optional": true }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", + "optional": true }, - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", + "@rushstack/node-core-library": { + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.38.0.tgz", + "integrity": "sha512-cmvl0yQx8sSmbuXwiRYJi8TO+jpTtrLJQ8UmFHhKvgPVJAW8cV8dnpD1Xx/BvTGrJZ2XtRAIkAhBS9okBnap4w==", "dev": true, "requires": { - "type-detect": "4.0.8" + "@types/node": "10.17.13", + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.17.0", + "semver": "~7.3.0", + "timsort": "~0.3.0", + "z-schema": "~3.18.3" }, "dependencies": { - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "@types/node": { + "version": "10.17.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz", + "integrity": "sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg==", + "dev": true + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, - "@sinonjs/formatio": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", - "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "@rushstack/rig-package": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.2.12.tgz", + "integrity": "sha512-nbePcvF8hQwv0ql9aeQxcaMPK/h1OLAC00W7fWCRWIvD2MchZOE8jumIIr66HGrfG2X1sw++m/ZYI4D+BM5ovQ==", + "dev": true, + "requires": { + "resolve": "~1.17.0", + "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 + } + } + }, + "@rushstack/ts-command-line": { + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.7.10.tgz", + "integrity": "sha512-8t042g8eerypNOEcdpxwRA3uCmz0duMo21rG4Z2mdz7JxJeylDmzjlU3wDdef2t3P1Z61JCdZB6fbm1Mh0zi7w==", + "dev": true, + "requires": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", "dev": true, "requires": { - "samsam": "1.3.0" + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" } }, "@sinonjs/samsam": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", - "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz", + "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==", "dev": true, "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, "@sinonjs/text-encoding": { @@ -617,6 +1221,18 @@ "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", "dev": true }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true + }, + "@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, "@types/bcrypt": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-2.0.0.tgz", @@ -624,159 +1240,234 @@ "dev": true }, "@types/bluebird": { - "version": "3.5.24", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.24.tgz", - "integrity": "sha512-YeQoDpq4Lm8ppSBqAnAeF/xy1cYp/dMTif2JFcvmAbETMRlvKHT2iLcWu+WyYiJO3b3Ivokwo7EQca/xfLVJmg==", + "version": "3.5.35", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.35.tgz", + "integrity": "sha512-2WeeXK7BuQo7yPI4WGOBum90SzF/f8rqlvpaXx4rjeTmNssGRDHWf7fgDUH90xMB3sUOu716fUK5d+OVx0+ncQ==", "dev": true }, + "@types/body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, "@types/caseless": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.1.tgz", - "integrity": "sha512-FhlMa34NHp9K5MY1Uz8yb+ZvuX0pnvn3jScRSNAb75KHGB8d3rEU6hqMs3Z2vjuytcMfRg6c5CHMc3wtYyD2/A==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", + "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==", "dev": true }, "@types/chai": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-3.5.2.tgz", - "integrity": "sha1-wRzSgX06QBt7oPWkIPNcVhObHB4=", + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.21.tgz", + "integrity": "sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==", "dev": true }, "@types/chai-as-promised": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-0.0.29.tgz", - "integrity": "sha1-Q9UokqqZjhhaPePiR37bhXO+HXc=", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz", + "integrity": "sha512-FQnh1ohPXJELpKhzjuDkPLR2BZCAqed+a6xV4MI/T3XzHfd2FlarfUGUdZYgqYe8oxkYn0fchHEeHfHqdZ96sg==", "dev": true, "requires": { - "@types/chai": "*", - "@types/promises-a-plus": "*" + "@types/chai": "*" } }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, - "@types/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=", + "@types/connect": { + "version": "3.4.34", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", + "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", "dev": true }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, + "@types/express": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.12.tgz", + "integrity": "sha512-pTYas6FrP15B1Oa0bkN5tQMNqOcVXa9j4FTFtO8DWI9kppKib+6NJtfTOOLcwxuuYvcX2+dVG6et1SxW/Kc17Q==", "requires": { - "@types/node": "*" + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.22.tgz", + "integrity": "sha512-WdqmrUsRS4ootGha6tVwk/IVHM1iorU8tGehftQD2NWiPniw/sm7xdJOIlXLwqdInL9wBw/p7oO8vaYEF3NDmA==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "requires": { + "@types/express": "*" } }, + "@types/firebase-token-generator": { + "version": "2.0.28", + "resolved": "http://registry.npmjs.org/@types/firebase-token-generator/-/firebase-token-generator-2.0.28.tgz", + "integrity": "sha1-Z1VIHZMk4mt6XItFXWgUg3aCw5Y=", + "dev": true + }, "@types/json-schema": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", - "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", + "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", "dev": true }, "@types/jsonwebtoken": { - "version": "7.2.8", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-7.2.8.tgz", - "integrity": "sha512-XENN3YzEB8D6TiUww0O8SRznzy1v+77lH7UmuN54xq/IHIsyWjWOzZuFFTtoiRuaE782uAoRwBe/wwow+vQXZw==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-rNAPdomlIUX0i0cg2+I+Q1wOUr531zHBQ+cV/28PJ39bSPKjahatZZ2LMuhiguETkCgLVzfruw/ZvNMNkKoSzw==", "dev": true, "requires": { "@types/node": "*" } }, "@types/lodash": { - "version": "4.14.118", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.118.tgz", - "integrity": "sha512-iiJbKLZbhSa6FYRip/9ZDX6HXhayXLDGY2Fqws9cOkEQ6XeKfaxB0sC541mowZJueYyMnVUmmG+al5/4fCDrgw==", + "version": "4.14.157", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.157.tgz", + "integrity": "sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ==", "dev": true }, "@types/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", + "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==", + "optional": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-1z8k4wzFnNjVK/tlxvrWuK5WMt6mydWWP7+zvH5eFep4oj+UkrfiJTRtjCeBXNpwaA/FYqqtb4/QS4ianFpIRA==", "dev": true }, "@types/minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=", + "version": "1.2.1", + "resolved": "http://registry.npmjs.org/@types/minimist/-/minimist-1.2.1.tgz", + "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz", + "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", "dev": true }, "@types/nock": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-9.3.0.tgz", - "integrity": "sha512-ZHf/X8rTQ5Tb1rHjxIJYqm55uO265agE3G7NoSXVa2ep+EcJXgB2fsme+zBvK7MhrxTwkC/xkB6THyv50u0MGw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", + "integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", "dev": true, "requires": { - "@types/node": "*" + "nock": "*" } }, "@types/node": { - "version": "8.10.59", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.59.tgz", - "integrity": "sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ==" + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz", + "integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA==" }, - "@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/qs": { + "version": "6.9.6", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", + "integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, "@types/request": { - "version": "2.48.1", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", - "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==", + "version": "2.48.5", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz", + "integrity": "sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ==", "dev": true, "requires": { "@types/caseless": "*", - "@types/form-data": "*", "@types/node": "*", - "@types/tough-cookie": "*" + "@types/tough-cookie": "*", + "form-data": "^2.5.0" } }, "@types/request-promise": { - "version": "4.1.42", - "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.42.tgz", - "integrity": "sha512-b8li55sEZ00BXZstZ3d8WOi48dnapTqB1VufEG9Qox0nVI2JVnTVT1Mw4JbBa1j+1sGVX/qJ0R4WDv4v2GjT0w==", + "version": "4.1.47", + "resolved": "https://registry.npmjs.org/@types/request-promise/-/request-promise-4.1.47.tgz", + "integrity": "sha512-eRSZhAS8SMsrWOM8vbhxFGVZhTbWSJvaRKyufJTdIf4gscUouQvOBlfotPSPHbMR3S7kfkyKbhb1SWPmQdy3KQ==", "dev": true, "requires": { "@types/bluebird": "*", "@types/request": "*" } }, - "@types/scrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/scrypt/-/scrypt-6.0.0.tgz", - "integrity": "sha512-kiQtYPL3YOOliArRkiE58O5DK7NyURo81hU4G43+wyQp4UiqGfM7muHGAZ/nHOU8LdAICiwcm28y5BPBZXWIYg==", - "dev": true, + "@types/serve-static": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.9.tgz", + "integrity": "sha512-ZFqF6qa48XsPdjXV5Gsz0Zqmux2PerNd3a/ktL45mHpa19cuMi/cL8tcxdAx497yRh+QtYPuofjT9oWw9P7nkA==", "requires": { + "@types/mime": "^1", "@types/node": "*" } }, "@types/sinon": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-4.3.3.tgz", - "integrity": "sha512-Tt7w/ylBS/OEAlSCwzB0Db1KbxnkycP/1UkQpbvKFYoUuRn4uYsC3xh5TRPrOjTy0i8TIkSz1JdNL4GPVdf3KQ==", - "dev": true + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.2.tgz", + "integrity": "sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==", + "dev": true, + "requires": { + "@sinonjs/fake-timers": "^7.1.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } + } }, "@types/sinon-chai": { - "version": "2.7.34", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.34.tgz", - "integrity": "sha512-1AxQR0Tk4Q0FYGoJrj66UKhiMmlmxg78SuC5rBJp55Nex4cd948GGJca1Qf2bf1X7P4YKcAW3uKiQB9yvJP4rA==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.5.tgz", + "integrity": "sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==", "dev": true, "requires": { "@types/chai": "*", @@ -784,62 +1475,51 @@ } }, "@types/tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-Set5ZdrAaKI/qHdFlVMgm/GsAv/wkXhSTuZFkJ+JI7HK+wIkIlOaUXSXieIvJ0+OvGIqtREFoE+NHJtEq0gtEw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.20.0.tgz", - "integrity": "sha512-cimIdVDV3MakiGJqMXw51Xci6oEDEoPkvh8ggJe2IIzcc0fYqAxOXN6Vbeanahz6dLZq64W+40iUEc9g32FLDQ==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz", + "integrity": "sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "2.20.0", - "eslint-utils": "^1.4.3", + "@typescript-eslint/experimental-utils": "2.34.0", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", "tsutils": "^3.17.1" - }, - "dependencies": { - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - } } }, "@typescript-eslint/experimental-utils": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.20.0.tgz", - "integrity": "sha512-fEBy9xYrwG9hfBLFEwGW2lKwDRTmYzH3DwTmYbT+SMycmxAoPl0eGretnBFj/s+NfYBG63w/5c3lsvqqz5mYag==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", + "integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "2.20.0", - "eslint-scope": "^5.0.0" + "@typescript-eslint/typescript-estree": "2.34.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.20.0.tgz", - "integrity": "sha512-o8qsKaosLh2qhMZiHNtaHKTHyCHc3Triq6aMnwnWj7budm3xAY9owSZzV1uon5T9cWmJRJGzTFa90aex4m77Lw==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.34.0.tgz", + "integrity": "sha512-03ilO0ucSD0EPTw2X4PntSIRFtDPWjrVq7C3/Z3VQHRC7+13YB55rcJI3Jt+YgeHbjUdJPcPa7b23rXCBokuyA==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "2.20.0", - "@typescript-eslint/typescript-estree": "2.20.0", + "@typescript-eslint/experimental-utils": "2.34.0", + "@typescript-eslint/typescript-estree": "2.34.0", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/typescript-estree": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.20.0.tgz", - "integrity": "sha512-WlFk8QtI8pPaE7JGQGxU7nGcnk1ccKAJkhbVookv94ZcAef3m6oCE/jEDL6dGte3JcD7reKrA0o55XhBRiVT3A==", + "version": "2.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", + "integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==", "dev": true, "requires": { "debug": "^4.1.1", @@ -847,80 +1527,55 @@ "glob": "^7.1.6", "is-glob": "^4.0.1", "lodash": "^4.17.15", - "semver": "^6.3.0", + "semver": "^7.3.2", "tsutils": "^3.17.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", "dev": true - }, - "tsutils": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", - "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } } } }, + "@ungap/promise-all-settled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", + "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", + "dev": true + }, "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "abbrev": { + "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" } }, "acorn": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", - "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", "dev": true }, "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", "dev": true, "requires": { "acorn": "^6.0.1", @@ -936,32 +1591,43 @@ } }, "acorn-jsx": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", - "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", + "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", "dev": true }, "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", "dev": true }, "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "optional": true, + "requires": { + "debug": "4" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "requires": { - "es6-promisify": "^5.0.0" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" } }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", + "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -976,13 +1642,30 @@ "ansi-wrap": "^0.1.0" } }, + "ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-escapes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.0.tgz", - "integrity": "sha512-EiYhwo0v255HUL6eDyuLrXEkTi7WwVCLAw+SeOQ7M7qdun1z1pum4DEm/nuqIVbPvi9RPPc9k9LbyBv6H0DwVg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", "dev": true, "requires": { - "type-fest": "^0.8.1" + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } } }, "ansi-gray": { @@ -994,6 +1677,15 @@ "ansi-wrap": "0.1.0" } }, + "ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -1001,10 +1693,27 @@ "dev": true }, "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + }, + "dependencies": { + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } }, "ansi-wrap": { "version": "0.1.0", @@ -1038,20 +1747,42 @@ } }, "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", "dev": true, "requires": { - "default-require-extensions": "^2.0.0" + "default-require-extensions": "^3.0.0" } }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "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", @@ -1098,9 +1829,9 @@ "dev": true }, "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", "dev": true }, "array-each": { @@ -1115,18 +1846,6 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, "array-initial": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", @@ -1162,18 +1881,6 @@ } } }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", @@ -1200,18 +1907,9 @@ } }, "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, "array-unique": { @@ -1221,10 +1919,9 @@ "dev": true }, "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, "asn1": { "version": "0.2.4", @@ -1277,12 +1974,6 @@ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", "dev": true }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==", - "dev": true - }, "async-settle": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", @@ -1298,6 +1989,12 @@ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -1311,9 +2008,9 @@ "dev": true }, "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, "bach": { @@ -1333,15 +2030,6 @@ "now-and-later": "^2.0.0" } }, - "backbone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", - "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", - "dev": true, - "requires": { - "underscore": ">=1.8.3" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -1404,2180 +2092,786 @@ } }, "base64-js": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", - "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" }, "bcrypt": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.2.tgz", - "integrity": "sha512-kE1IaaRchCgdrmzQX/eBQKcsuL4jRHZ+O11sMvEUrI/HgFTQYAGvxlj9z7kb3zfFuwljQ5y8/NrbnXtgx5oJLg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.0.1.tgz", + "integrity": "sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw==", + "dev": true, + "requires": { + "@mapbox/node-pre-gyp": "^1.0.0", + "node-addon-api": "^3.1.0" + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", + "optional": true + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "dev": true + }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "requires": { - "nan": "2.11.1", - "node-pre-gyp": "0.11.0" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", - "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } - } - }, - "minizlib": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", - "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "needle": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.3.tgz", - "integrity": "sha512-GPL22d/U9cai87FcCPO6e+MT3vyHS2j+zwotakDc7kE2DtUAqFKMXLJCTtRp+5S75vXIwQPvIxkvlctxf9q4gQ==", - "dev": true, - "requires": { - "debug": "^2.1.2", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "dev": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", - "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==", - "dev": true - }, - "npm-packlist": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", - "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } - } - }, - "readable-stream": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.5.tgz", - "integrity": "sha512-tK0yDhrkygt/knjowCUiWP9YdV7c5R+8cR0r/kt9ZhBU906Fs6RpQJCEilamRJj1Nx2rWI6LkW9gKqjTkshhEw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.0.3", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, - "tar": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.6.tgz", - "integrity": "sha512-tMkTnh9EdzxyfW+6GK6fCahagXsnYk6kE6S9Gr9pjVdys769+laCTbodXDhPAjzVtEBazRgP0gYqOjnk9dQzLg==", - "dev": true, - "requires": { - "chownr": "^1.0.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.3", - "minizlib": "^1.1.0", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "yallist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "beeper": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", - "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", - "dev": true - }, - "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "binaryextensions": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/binaryextensions/-/binaryextensions-1.0.1.tgz", - "integrity": "sha1-HmN0iLNbWL2l9HdL+WpSEqjJB1U=", - "dev": true - }, - "bluebird": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", - "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "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 - }, - "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - } - } - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "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": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "bun": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/bun/-/bun-0.0.12.tgz", - "integrity": "sha512-Toms18J9DqnT+IfWkwxVTB2EaBprHvjlMWrTIsfX4xbu3ZBqVBwrERU0em1IgtRe04wT+wJxMlKHZok24hrcSQ==", - "optional": true, - "requires": { - "readable-stream": "~1.0.32" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "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" - } - }, - "caching-transform": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", - "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", - "dev": true, - "requires": { - "hasha": "^3.0.0", - "make-dir": "^2.0.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.4.2" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "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": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "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.1", - "deep-eql": "^0.1.3", - "type-detect": "^1.0.0" - } - }, - "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=", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "child-process-promise": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", - "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", - "dev": true, - "requires": { - "cross-spawn": "^4.0.2", - "node-version": "^1.0.0", - "promise-polyfill": "^6.0.1" - }, - "dependencies": { - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } - } - }, - "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "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 - } - } - }, - "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==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.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=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "requires": { - "restore-cursor": "^3.1.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - } - }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, - "clone-stats": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", - "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.2.tgz", - "integrity": "sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "collection-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", - "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", - "dev": true, - "requires": { - "arr-map": "^2.0.2", - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "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": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "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 - }, - "combined-stream": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", - "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "compressible": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz", - "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==", - "optional": true, - "requires": { - "mime-db": ">= 1.40.0 < 2" - }, - "dependencies": { - "mime-db": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", - "optional": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "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": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "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": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "configstore": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.0.tgz", - "integrity": "sha512-eE/hvMs7qw7DlcB5JPRnthmrITuHMmACUJAp89v6PT6iOqzoLS7HRWhBtuHMlhNHo2AhUSA/3Dh1bKNJHcublQ==", - "optional": true, - "requires": { - "dot-prop": "^5.1.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "write-file-atomic": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", - "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", - "optional": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - } - } - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-props": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", - "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", - "dev": true, - "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" - } - }, - "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=" - }, - "cp-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", - "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "make-dir": "^2.0.0", - "nested-error-stacks": "^2.0.0", - "pify": "^4.0.1", - "safe-buffer": "^5.0.1" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "optional": true - }, - "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "dev": true - }, - "cssstyle": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", - "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, - "requires": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "date-and-time": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.10.0.tgz", - "integrity": "sha512-IbIzxtvK80JZOVsWF6+NOjunTaoFVYxkAQoyzmflJyuRCJAJebehy48mPiCAedcGp4P7/UO3QYRWa0fe6INftg==", - "optional": true - }, - "dateformat": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", - "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=", - "dev": true - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": 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=", - "dev": true - }, - "deep-eql": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", - "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", - "dev": true, - "requires": { - "type-detect": "0.1.1" - }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", - "dev": true - }, - "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 - }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "requires": { - "kind-of": "^5.0.2" - }, - "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==", - "dev": true - } - } - }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } - } - }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" + "is-extendable": "^0.1.0" } } } }, - "del": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", - "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", - "dev": true, - "requires": { - "globby": "^5.0.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "rimraf": "^2.2.8" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "delayed-stream": { + "browser-process-hrtime": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "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 }, - "dicer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", - "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, "requires": { - "streamsearch": "0.1.2" + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" } }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "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": { - "esutils": "^2.0.2" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } }, - "dom-storage": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", - "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==", + "buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", "dev": true }, - "domexception": { + "buffer-equal-constant-time": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, - "dot-prop": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz", - "integrity": "sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A==", - "optional": true, - "requires": { - "is-obj": "^2.0.0" - } + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "duplexer2": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", - "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "dev": true, "requires": { - "readable-stream": "~1.1.9" - }, - "dependencies": { - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } + "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" } }, - "duplexify": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", - "integrity": "sha512-vM58DwdnKmty+FSPzT14K9JXb90H+j5emaR4KYbr2KTIz00WHGbWOe5ghQTx233ZCLZtrGDALzKwcjEtSt35mA==", + "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": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" } }, - "each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001228", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001228.tgz", + "integrity": "sha512-QQmLOGJ3DEgokHbMSA8cj2a+geXqmnpyOFT0lhQV6P3/YOJvGDEwoedcwxEQ30gJIwIIunHIicunJ2rzK5gB2A==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", "dev": true, "requires": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "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": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "check-error": "^1.0.2" } }, - "ecdsa-sig-formatter": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", - "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, "requires": { - "safe-buffer": "^5.0.1" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", - "optional": true + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true }, - "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==", + "child-process-promise": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/child-process-promise/-/child-process-promise-2.2.1.tgz", + "integrity": "sha1-RzChHvYQ+tRQuPIjx50x172tgHQ=", "dev": true, "requires": { - "is-arrayish": "^0.2.1" + "cross-spawn": "^4.0.2", + "node-version": "^1.0.0", + "promise-polyfill": "^6.0.1" } }, - "es-abstract": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", - "integrity": "sha512-C8Fx/0jFmV5IPoMOFPA9P9G5NtqW+4cOPit3MIuvR2t7Ag2K15EJTpxnHAYTzL+aYQJIESYeXZmDBfOBE1HcpA==", + "chokidar": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "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" + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + }, + "dependencies": { + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + } } }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true }, - "es5-ext": { - "version": "0.10.50", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.50.tgz", - "integrity": "sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw==", + "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==", "dev": true, "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "^1.0.0" + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.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=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.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==", + "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 }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "dev": true, "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" + "restore-cursor": "^3.1.0" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "requires": { - "es6-promise": "^4.0.3" - } + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", "dev": true, "requires": { - "d": "1", - "es5-ext": "~0.10.14" + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" } }, - "es6-weak-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", - "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.46", - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.1" - } + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", + "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": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", "dev": true }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "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": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" } }, - "eslint": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", - "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.4.3", - "eslint-visitor-keys": "^1.1.0", - "espree": "^6.1.2", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^7.0.0", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.3", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, - "glob-parent": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", - "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.3.0.tgz", - "integrity": "sha512-wAfjdLgFsPZsklLJvOBUBmzYE8/CwhEqSBEMRXA3qxIiNtyqvjYurAtIfDh6chlEPUfmTY3MnZh5Hfh4q0UlIw==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "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" - } - } + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-map/-/collection-map-1.0.0.tgz", + "integrity": "sha1-rqDwb40mx4DCt1SUOFVEsiVa8Yw=", + "dev": true, + "requires": { + "arr-map": "^2.0.2", + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" } }, - "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "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": { - "eslint-visitor-keys": "^1.1.0" + "color-name": "1.1.3" } }, - "eslint-visitor-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", - "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "espree": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", - "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "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 + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "requires": { - "acorn": "^7.1.0", - "acorn-jsx": "^5.1.0", - "eslint-visitor-keys": "^1.1.0" + "delayed-stream": "~1.0.0" } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "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 + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "esquery": { + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "optional": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "optional": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "concat-with-sourcemaps": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz", - "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==", + "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": { - "estraverse": "^4.0.0" + "source-map": "^0.6.1" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "optional": true, "requires": { - "estraverse": "^4.1.0" + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", "dev": true }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } }, - "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==" + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "copy-props": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", + "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", "dev": true, "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" + "each-props": "^1.3.0", + "is-plain-object": "^2.0.1" } }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "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=" + }, + "cross-spawn": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", + "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", "dev": true, "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "lru-cache": "^4.0.1", + "which": "^1.2.9" }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "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": { - "is-descriptor": "^0.1.0" + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "optional": true + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true } } }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "date-and-time": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-0.14.2.tgz", + "integrity": "sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA==", + "optional": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": 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=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", "dev": true, "requires": { - "homedir-polyfill": "^1.0.1" + "type-detect": "^4.0.0" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "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 }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", "dev": true, "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" + "kind-of": "^5.0.2" }, "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==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } + "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==", + "dev": true } } }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "default-require-extensions": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", + "integrity": "sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==", "dev": true, "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" + "strip-bom": "^4.0.0" + }, + "dependencies": { + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + } } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "default-resolution": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", + "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { - "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.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "object-keys": "^1.0.12" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, "is-accessor-descriptor": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", @@ -3609,1046 +2903,1245 @@ } } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "requires": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "dependencies": { + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, - "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", - "dev": true, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true + }, + "dicer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", + "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==", "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "time-stamp": "^1.0.0" + "streamsearch": "0.1.2" } }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, - "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=", + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + }, + "dependencies": { + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + } + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-storage": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/dom-storage/-/dom-storage-2.1.0.tgz", + "integrity": "sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q==", "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=", + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "optional": true, + "requires": { + "is-obj": "^2.0.0" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "each-props": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", + "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.1", + "object.defaults": "^1.1.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "electron-to-chromium": { + "version": "1.3.736", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.736.tgz", + "integrity": "sha512-DY8dA7gR51MSo66DqitEQoUMQ0Z+A2DSXFi7tK304bdTVqczCAfUuyQw6Wdg8hIoo5zIxkU1L24RQtUce1Ioig==", "dev": true }, - "fast-text-encoding": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz", - "integrity": "sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ==" + "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==" }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "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": { - "websocket-driver": ">=0.5.1" + "once": "^1.4.0" } }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "optional": true + }, + "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": { - "escape-string-regexp": "^1.0.5" + "is-arrayish": "^0.2.1" } }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", "dev": true, "requires": { - "flat-cache": "^2.0.1" + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "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": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "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 + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", "dev": true, "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" } }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" + "d": "^1.0.1", + "ext": "^1.1.2" } }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", "dev": true, "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" } }, - "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 + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "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 }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", "dev": true, "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" }, "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "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, - "requires": { - "glob": "^7.1.3" - } + "optional": true } } }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", - "dev": true - }, - "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==", + "eslint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", + "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", "dev": true, "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^12.1.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.3", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "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": { - "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" + "color-convert": "^1.9.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==", + "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": { - "safe-buffer": "~5.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "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.1" - } - }, - "foreground-child": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", - "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", - "dev": true, - "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - }, - "dependencies": { + }, "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "lru-cache": "^4.0.1", + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, - "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==", + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", "dev": true, "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "eslint-visitor-keys": "^1.1.0" } }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz", + "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==", "dev": true + }, + "supports-color": { + "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" + } } } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "eslint-scope": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", + "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } }, - "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==", + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", "dev": true, "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "eslint-visitor-keys": "^1.1.0" } }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + }, + "espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", "dev": true, "requires": { - "map-cache": "^0.2.2" + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" } }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "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 + }, + "esquery": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", + "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", "dev": true, "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "estraverse": "^5.1.0" }, "dependencies": { - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", + "estraverse": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.1.0.tgz", + "integrity": "sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==", "dev": true } } }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "dev": true, "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "estraverse": "^4.1.0" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "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 + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "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" + "ms": "2.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==", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "is-descriptor": "^0.1.0" } }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "is-extendable": "^0.1.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "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" + } }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", "dev": true, - "optional": true, "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" + "type": "^2.0.0" }, "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, + "type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "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==", "dev": true, - "optional": true, "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" + "is-plain-object": "^2.0.4" } - }, - "glob": { - "version": "7.1.3", - "bundled": true, + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "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.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, - "optional": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "is-descriptor": "^1.0.0" } }, - "has-unicode": { + "extend-shallow": { "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { - "once": "^1.3.0", - "wrappy": "1" + "is-extendable": "^0.1.0" } }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { + "is-accessor-descriptor": { "version": "1.0.0", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "number-is-nan": "^1.0.0" + "kind-of": "^6.0.0" } }, - "isarray": { + "is-data-descriptor": { "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "kind-of": "^6.0.0" } }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "minipass": { - "version": "2.3.5", - "bundled": 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==", "dev": true, "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "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" + } + }, + "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-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "optional": true, "requires": { - "minipass": "^2.2.1" + "fill-range": "^7.0.1" } }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "minimist": "0.0.8" + "to-regex-range": "^5.0.1" } }, - "ms": { - "version": "2.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", - "dev": true, - "optional": true + "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 }, - "needle": { - "version": "2.3.0", - "bundled": true, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, - "optional": true, "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "dependencies": { + "picomatch": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", + "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "dev": true + } } }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "optional": true, "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" + "is-number": "^7.0.0" } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, + } + } + }, + "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": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fast-text-encoding": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz", + "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==", + "optional": true + }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, - "optional": true, "requires": { - "abbrev": "1", - "osenv": "^0.1.4" + "is-extendable": "^0.1.0" } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, + } + } + }, + "find-cache-dir": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + } + }, + "firebase-token-generator": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/firebase-token-generator/-/firebase-token-generator-2.0.0.tgz", + "integrity": "sha1-l2fXWewTq9yZuhFf1eqZ2Lk9EgY=", + "dev": true + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", "dev": true, - "optional": true, "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "glob": "^7.1.3" } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, + } + } + }, + "flatted": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", + "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", + "dev": true + }, + "flush-write-stream": { + "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": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "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.1" + } + }, + "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, - "optional": true, "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, + "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 }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "wrappy": "1" + "shebang-regex": "^3.0.0" } }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true + "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 }, - "osenv": { - "version": "0.1.5", - "bundled": true, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "optional": true, "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "isexe": "^2.0.0" } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true + }, + "fs-extra": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz", + "integrity": "sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "dependencies": { + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "optional": true, "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" }, "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true } } }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, + "universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, + } + } + }, + "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": { + "minipass": "^3.0.0" + } + }, + "fs-mkdirp-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", + "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, - "optional": true, "requires": { - "string-width": "^1.0.2 || 2" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "dev": true } } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "dev": true, + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true }, "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, "gaxios": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-2.1.0.tgz", - "integrity": "sha512-Gtpb5sdQmb82sgVkT2GnS2n+Kx4dlFwbeMYcDlD395aEvsLCSQXJJcHt7oJ2LrGxDEAeiOkK79Zv2A8Pzt6CFg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-3.0.3.tgz", + "integrity": "sha512-PkzQludeIFhd535/yucALT/Wxyj/y2zLyrMwPcJmnLHDugmV49NvAi/vb+VUq/eWztATZCNcb8ue+ywPG+oLuw==", + "optional": true, "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", - "https-proxy-agent": "^3.0.0", + "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", "node-fetch": "^2.3.0" - }, - "dependencies": { - "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" - } } }, "gcp-metadata": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-3.2.1.tgz", - "integrity": "sha512-JjDedBWnbXVXWwTpjBdpb9RpVLiowXG4/50rra4hPH8REXAi2si6Xbb48B2SwkQBLz9Wu6+o32GDTvVy2kkLoQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.1.0.tgz", + "integrity": "sha512-r57SV28+olVsflPlKyVig3Muo/VDlcsObMtvDGOEtEJXj+DDE8bEl0coIkXh//hbkSDTvo+f5lbihZOndYXQQQ==", + "optional": true, "requires": { - "gaxios": "^2.1.0", + "gaxios": "^3.0.0", "json-bigint": "^0.3.0" } }, "gcs-resumable-upload": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-2.3.0.tgz", - "integrity": "sha512-PclXJiEngrVx0c4K0LfE1XOxhmOkBEy39Rrhspdn6jAbbwe4OQMZfjo7Z1LHBrh57+bNZeIN4M+BooYppCoHSg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-3.1.1.tgz", + "integrity": "sha512-RS1osvAicj9+MjCc6jAcVL1Pt3tg7NK2C2gXM5nqD1Gs0klF2kj5nnAFSBy97JrtslMIQzpb7iSuxaG8rFWd2A==", "optional": true, "requires": { "abort-controller": "^3.0.0", "configstore": "^5.0.0", - "gaxios": "^2.0.0", - "google-auth-library": "^5.0.0", + "extend": "^3.0.2", + "gaxios": "^3.0.0", + "google-auth-library": "^6.0.0", "pumpify": "^2.0.0", "stream-events": "^1.0.4" - }, - "dependencies": { - "duplexify": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", - "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", - "optional": true, - "requires": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "optional": true, - "requires": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "optional": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "requires": { - "safe-buffer": "~5.2.0" - } - } } }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", @@ -4661,33 +4154,18 @@ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", "dev": true }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, "get-prop": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/get-prop/-/get-prop-0.0.10.tgz", "integrity": "sha1-p2tANdFw3XKKZkVpVbyBkjZzCNI=", "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -4704,9 +4182,9 @@ } }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -4718,24 +4196,12 @@ } }, "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", + "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", "dev": true, "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } + "is-glob": "^4.0.1" } }, "glob-stream": { @@ -4756,34 +4222,44 @@ "unique-stream": "^2.0.2" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true, + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } } } @@ -4827,113 +4303,212 @@ } }, "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } }, "globby": { - "version": "5.0.0", - "resolved": "http://registry.npmjs.org/globby/-/globby-5.0.0.tgz", - "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "version": "11.0.3", + "resolved": "http://registry.npmjs.org/globby/-/globby-11.0.3.tgz", + "integrity": "sha512-ffdmosjA807y7+lA1NM0jELARVmYul/715xiILEjo3hBLPTcirgQNnXECn5g3mtR8TOLCVbkfua1Hpen25/Xcg==", "dev": true, "requires": { - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" }, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", "dev": true } } }, "glogg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", + "integrity": "sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA==", "dev": true, "requires": { "sparkles": "^1.0.0" } }, "google-auth-library": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-5.5.1.tgz", - "integrity": "sha512-zCtjQccWS/EHYyFdXRbfeSGM/gW+d7uMAcVnvXRnjBXON5ijo6s0nsObP0ifqileIDSbZjTlLtgo+UoN8IFJcg==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-6.0.3.tgz", + "integrity": "sha512-2Np6ojPmaJGXHSMsBhtTQEKfSMdLc8hefoihv7N2cwEr8E5bq39fhoat6TcXHwa0XoGO5Guh9sp3nxHFPmibMw==", + "optional": true, "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", "fast-text-encoding": "^1.0.0", - "gaxios": "^2.1.0", - "gcp-metadata": "^3.2.0", - "gtoken": "^4.1.0", - "jws": "^3.1.5", + "gaxios": "^3.0.0", + "gcp-metadata": "^4.1.0", + "gtoken": "^5.0.0", + "jws": "^4.0.0", "lru-cache": "^5.0.0" - }, - "dependencies": { - "arrify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", - "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" - } } }, "google-gax": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-1.12.0.tgz", - "integrity": "sha512-BeeoxVO6y9K20gUsexUwptutd0PfrTItrA02JWwwstlBIOAcvgFp86MHWufQsnrkPVhxBjHXq65aIkSejtJjDg==", + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.17.1.tgz", + "integrity": "sha512-CoR7OYuEzIDt3mp7cLYL+oGPmYdImS1WEwIvjF0zk0LhEBBmvRjWHTpBwazLGsT8hz+6zPh/4fjIjNaUxzIvzg==", "optional": true, "requires": { - "@grpc/grpc-js": "^0.6.12", - "@grpc/proto-loader": "^0.5.1", + "@grpc/grpc-js": "~1.3.0", + "@grpc/proto-loader": "^0.6.1", "@types/long": "^4.0.0", "abort-controller": "^3.0.0", - "duplexify": "^3.6.0", - "google-auth-library": "^5.0.0", + "duplexify": "^4.0.0", + "fast-text-encoding": "^1.0.3", + "google-auth-library": "^7.3.0", "is-stream-ended": "^0.1.4", - "lodash.at": "^4.6.0", - "lodash.has": "^4.5.2", - "node-fetch": "^2.6.0", - "protobufjs": "^6.8.8", - "retry-request": "^4.0.0", - "semver": "^6.0.0", - "walkdir": "^0.4.0" + "node-fetch": "^2.6.1", + "object-hash": "^2.1.1", + "protobufjs": "^6.10.2", + "retry-request": "^4.0.0" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "gaxios": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", + "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "optional": true, + "requires": { + "abort-controller": "^3.0.0", + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.3.0" + } + }, + "gcp-metadata": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.0.tgz", + "integrity": "sha512-L9XQUpvKJCM76YRSmcxrR4mFPzPGsgZUH+GgHMxAET8qc6+BhRJq63RLhWakgEO2KKVgeSDVfyiNjkGSADwNTA==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "json-bigint": "^1.0.0" + } + }, + "google-auth-library": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.3.0.tgz", + "integrity": "sha512-MPeeMlnsYnoiiVFMwX3hgaS684aiXrSqKoDP+xL4Ejg4Z0qLvIeg4XsaChemyFI8ZUO7ApwDAzNtgmhWSDNh5w==", + "optional": true, + "requires": { + "arrify": "^2.0.0", + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "fast-text-encoding": "^1.0.0", + "gaxios": "^4.0.0", + "gcp-metadata": "^4.2.0", + "gtoken": "^5.0.4", + "jws": "^4.0.0", + "lru-cache": "^6.0.0" + } + }, + "google-p12-pem": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.0.tgz", + "integrity": "sha512-JUtEHXL4DY/N+xhlm7TC3qL797RPAtk0ZGXNs3/gWyiDHYoA/8Rjes0pztkda+sZv4ej1EoO2KhWgW5V9KTrSQ==", + "optional": true, + "requires": { + "node-forge": "^0.10.0" + } + }, + "gtoken": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.0.tgz", + "integrity": "sha512-mCcISYiaRZrJpfqOs0QWa6lfEM/C1V9ASkzFmuz43XBb5s1Vynh+CZy1ECeeJXVGx2PRByjYzb4Y4/zr1byr0w==", + "optional": true, + "requires": { + "gaxios": "^4.0.0", + "google-p12-pem": "^3.0.3", + "jws": "^4.0.0" + } + }, + "json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "requires": { + "bignumber.js": "^9.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "optional": true } } }, "google-p12-pem": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-2.0.2.tgz", - "integrity": "sha512-UfnEARfJKI6pbmC1hfFFm+UAcZxeIwTiEcHfqKe/drMsXD/ilnVjF7zgOGpHXyhuvX6jNJK3S8A0hOQjwtFxEw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.1.tgz", + "integrity": "sha512-VlQgtozgNVVVcYTXS36eQz4PXPt9gIPqLOhHN0QiV6W6h4qSCNVKPtKC5INtJsaHHF2r7+nOIa26MJeJMTaZEQ==", + "optional": true, "requires": { "node-forge": "^0.9.0" }, "dependencies": { "node-forge": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.1.tgz", - "integrity": "sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==" + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.2.tgz", + "integrity": "sha512-naKSScof4Wn+aoHU6HBsifh92Zeicm1GDQKd1vp3Y/kOi8ub0DozCa9KpvYNCXslFHYRmLNiqRopGdTGwNLpNw==", + "optional": true } } }, "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "growl": { "version": "1.10.5", @@ -4942,13 +4517,14 @@ "dev": true }, "gtoken": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-4.1.1.tgz", - "integrity": "sha512-2FEmEDGi4NdM6u+mtaLjSDDtHiw5wT+nBsI+yrSeFO6fVqPEytYVF6uiIpRaOaZhRP+ozjYWuwwtMlrjAyTcYA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.0.1.tgz", + "integrity": "sha512-33w4FNDkUcyIOq/TqyC+drnKdI4PdXmWp9lZzssyEQKuvu9ZFN3KttaSnDKo52U3E51oujVGop93mKxmqO8HHg==", + "optional": true, "requires": { - "gaxios": "^2.1.0", - "google-p12-pem": "^2.0.0", - "jws": "^3.1.5", + "gaxios": "^3.0.0", + "google-p12-pem": "^3.0.0", + "jws": "^4.0.0", "mime": "^2.2.0" } }, @@ -4964,16 +4540,22 @@ "vinyl-fs": "^3.0.0" }, "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } }, "gulp-cli": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -4984,7 +4566,7 @@ "copy-props": "^2.0.1", "fancy-log": "^1.3.2", "gulplog": "^1.0.0", - "interpret": "^1.1.0", + "interpret": "^1.4.0", "isobject": "^3.0.1", "liftoff": "^3.1.0", "matchdep": "^2.0.0", @@ -4992,144 +4574,71 @@ "pretty-hrtime": "^1.0.0", "replace-homedir": "^1.0.0", "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", + "v8flags": "^3.2.0", "yargs": "^7.1.0" } }, + "y18n": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==", + "dev": true + }, "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", + "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", "dev": true, "requires": { "camelcase": "^3.0.0", "cliui": "^3.2.0", "decamelize": "^1.1.1", - "get-caller-file": "^1.0.1", - "os-locale": "^1.4.0", - "read-pkg-up": "^1.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^1.0.2", - "which-module": "^1.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" - } - } - } - }, - "gulp-header": { - "version": "1.8.12", - "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", - "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", - "dev": true, - "requires": { - "concat-with-sourcemaps": "*", - "lodash.template": "^4.4.0", - "through2": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "5.0.0-security.0" } } } }, - "gulp-replace": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/gulp-replace/-/gulp-replace-0.5.4.tgz", - "integrity": "sha1-aaZ5FLvRPFYr/xT1BKQDeWqg2qk=", + "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": { - "istextorbinary": "1.0.2", - "readable-stream": "^2.0.1", - "replacestream": "^4.0.0" + "concat-with-sourcemaps": "^1.1.0", + "lodash.template": "^4.5.0", + "map-stream": "0.0.7", + "through2": "^2.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "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": { - "safe-buffer": "~5.1.0" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } } } @@ -5154,117 +4663,11 @@ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "dev": true }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } - } - }, - "gulp-util": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", - "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-uniq": "^1.0.2", - "beeper": "^1.0.0", - "chalk": "^1.0.0", - "dateformat": "^2.0.0", - "fancy-log": "^1.1.0", - "gulplog": "^1.0.0", - "has-gulplog": "^0.1.0", - "lodash._reescape": "^3.0.0", - "lodash._reevaluate": "^3.0.0", - "lodash._reinterpolate": "^3.0.0", - "lodash.template": "^3.0.0", - "minimist": "^1.1.0", - "multipipe": "^0.1.2", - "object-assign": "^3.0.0", - "replace-ext": "0.0.1", - "through2": "^2.0.0", - "vinyl": "^0.5.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } } } }, @@ -5278,15 +4681,16 @@ } }, "handlebars": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", - "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { "source-map": { @@ -5317,6 +4721,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5336,19 +4741,17 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, - "has-gulplog": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", - "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true }, "has-value": { "version": "1.0.0", @@ -5383,90 +4786,46 @@ } }, "hash-stream-validation": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz", - "integrity": "sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A==", - "optional": true, - "requires": { - "through2": "^2.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "optional": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "optional": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - } - } + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz", + "integrity": "sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ==", + "optional": true }, "hasha": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", - "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", "dev": true, "requires": { - "is-stream": "^1.0.1" + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" } }, "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, "highlight.js": { - "version": "9.15.10", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.15.10.tgz", - "integrity": "sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.5.0.tgz", + "integrity": "sha512-xTmvd9HiIHR6L53TMC7TKolEj65zG1XU+Onr8oi86mYa+nLcIbxTTWkpW7CsEwv/vK7u1zb8alZIMLDqqN6KTw==", "dev": true }, "homedir-polyfill": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", - "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "requires": { "parse-passwd": "^1.0.0" } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -5478,6 +4837,12 @@ "whatwg-encoding": "^1.0.1" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "http-message-parser": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/http-message-parser/-/http-message-parser-0.0.34.tgz", @@ -5489,21 +4854,36 @@ "get-prop": "0.0.10", "minimist": "^1.2.0", "stream-buffers": "^3.0.0" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + } } }, "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==" }, "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", "optional": true, "requires": { - "agent-base": "4", - "debug": "3.1.0" + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" } }, "http-signature": { @@ -5518,12 +4898,12 @@ } }, "https-proxy-agent": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", - "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "agent-base": "6", + "debug": "4" } }, "iconv-lite": { @@ -5536,9 +4916,9 @@ } }, "ieee754": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", - "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", "dev": true }, "ignore": { @@ -5557,11 +4937,23 @@ "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": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5573,34 +4965,34 @@ } }, "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, "inquirer": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.4.tgz", - "integrity": "sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.0.tgz", + "integrity": "sha512-K+LZp6L/6eE5swqIcVXrxl21aGDU4S50gKH0/d96OMQnSBCyGyZl/oZhbkVmdp5sBoINHd4xZvFSARh2dk6DWA==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", - "chalk": "^2.4.2", + "chalk": "^4.1.0", "cli-cursor": "^3.1.0", - "cli-width": "^2.0.0", + "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.15", "mute-stream": "0.0.8", - "run-async": "^2.2.0", - "rxjs": "^6.5.3", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", "string-width": "^4.1.0", - "strip-ansi": "^5.1.0", + "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "dependencies": { @@ -5611,29 +5003,44 @@ "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "is-fullwidth-code-point": { @@ -5651,51 +5058,32 @@ "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - } + "ansi-regex": "^5.0.0" } }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "invert-kv": { @@ -5740,12 +5128,6 @@ } } }, - "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "optional": true - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5767,20 +5149,21 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-builtin-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", - "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-core-module": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", + "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", "dev": true, "requires": { - "builtin-modules": "^1.0.0" + "has": "^1.0.3" } }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5802,9 +5185,10 @@ } }, "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", + "dev": true }, "is-descriptor": { "version": "0.1.6", @@ -5888,28 +5272,22 @@ "optional": true }, "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -5920,18 +5298,13 @@ "isobject": "^3.0.1" } }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, "requires": { - "has": "^1.0.1" + "has-symbols": "^1.0.1" } }, "is-relative": { @@ -5944,10 +5317,9 @@ } }, "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" }, "is-stream-ended": { "version": "0.1.4", @@ -5956,11 +5328,12 @@ "optional": true }, "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "dev": true, "requires": { - "has-symbols": "^1.0.0" + "has-symbols": "^1.0.1" } }, "is-typedarray": { @@ -5996,9 +5369,9 @@ "dev": true }, "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -6019,134 +5392,144 @@ "dev": true }, "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", "dev": true }, "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", "dev": true, "requires": { - "append-transform": "^1.0.0" + "append-transform": "^2.0.0" } }, "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", "dev": true, "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-processinfo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.2.tgz", + "integrity": "sha512-kOwpa7z9hme+IBPZMzQ5vdQj8srYgAtaRqeI48NGmAQ+/5yKiHLV0QbYqQpxsdEF0+w14SoB8YbnHKcXE2KnYw==", + "dev": true, + "requires": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.0", + "istanbul-lib-coverage": "^3.0.0-alpha.1", + "make-dir": "^3.0.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^3.3.3" }, "dependencies": { - "semver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", - "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } } } }, "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" }, "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } } } }, "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", "dev": true, "requires": { "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", + "istanbul-lib-coverage": "^3.0.0", "source-map": "^0.6.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6156,30 +5539,29 @@ } }, "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", "dev": true, "requires": { - "handlebars": "^4.1.2" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" } }, - "istextorbinary": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-1.0.2.tgz", - "integrity": "sha1-rOGTVNGpoBc+/rEITOD4ewrX3s8=", - "dev": true, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha1-o6vicYryQaKykE+EpiWXDzia4yo=", + "dev": true + }, + "jose": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz", + "integrity": "sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==", "requires": { - "binaryextensions": "~1.0.0", - "textextensions": "~1.0.0" + "@panva/asn1.js": "^1.0.0" } }, - "jquery": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", - "integrity": "sha512-Xb7SVYMvygPxbFMpTFQiHh1J7HClEaThguL15N/Gg37Lri/qKyhRGZYzHRyLH8Stq3Aow0LsHO2O2ci86fCrNQ==", - "dev": true - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6187,9 +5569,9 @@ "dev": true }, "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -6203,22 +5585,22 @@ "dev": true }, "jsdom": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.1.1.tgz", - "integrity": "sha512-cQZRBB33arrDAeCrAEWn1U3SvrvC8XysBua9Oqg1yWrsY/gYcusloJC3RZJXuY5eehSCmws8f2YeliCqGSkrtQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", "dev": true, "requires": { "abab": "^2.0.0", - "acorn": "^6.1.1", + "acorn": "^7.1.0", "acorn-globals": "^4.3.2", "array-equal": "^1.0.0", - "cssom": "^0.3.6", - "cssstyle": "^1.2.2", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", "data-urls": "^1.1.0", "domexception": "^1.0.1", "escodegen": "^1.11.1", "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.1.4", + "nwsapi": "^2.2.0", "parse5": "5.1.0", "pn": "^1.1.0", "request": "^2.88.0", @@ -6234,25 +5616,6 @@ "whatwg-url": "^7.0.0", "ws": "^7.0.0", "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - } } }, "jsesc": { @@ -6262,11 +5625,12 @@ "dev": true }, "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.1.tgz", + "integrity": "sha512-DGWnSzmusIreWlEupsUelHrhwmPPE+FiQvg+drKfk2p+bdEYa5mp4PJ8JsCWqae0M2jQNb0HPvnwvf1qOTThzQ==", + "optional": true, "requires": { - "bignumber.js": "^7.0.0" + "bignumber.js": "^9.0.0" } }, "json-parse-better-errors": { @@ -6299,6 +5663,15 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", @@ -6308,12 +5681,6 @@ "graceful-fs": "^4.1.6" } }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", - "dev": true - }, "jsonwebtoken": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", @@ -6331,14 +5698,6 @@ "semver": "^5.6.0" }, "dependencies": { - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -6358,10 +5717,10 @@ "safe-buffer": "^5.0.1" } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" } } }, @@ -6384,34 +5743,48 @@ "dev": true }, "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, "jwa": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", - "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, "requires": { "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.10", + "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, + "jwks-rsa": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.3.tgz", + "integrity": "sha512-/rkjXRWAp0cS00tunsHResw68P5iTQru8+jHufLNv3JHc4nObFEndfEUSuPugh09N+V9XYxKUqi7QrkmCHSSSg==", + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^4.1.0", + "jose": "^2.0.5", + "limiter": "^1.1.5", + "lru-memoizer": "^2.1.2" + } + }, "jws": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", - "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, "requires": { - "jwa": "^1.1.5", + "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, "last-run": { @@ -6431,38 +5804,6 @@ "dev": true, "requires": { "readable-stream": "^2.0.5" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "lcid": { @@ -6509,90 +5850,37 @@ "resolve": "^1.1.7" } }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "dependencies": { - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - } + "p-locate": "^4.1.0" } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basetostring": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", - "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", - "dev": true - }, - "lodash._basevalues": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", - "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash._reescape": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", - "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", - "dev": true - }, - "lodash._reevaluate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", - "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash._reinterpolate": { @@ -6601,32 +5889,16 @@ "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "dev": true }, - "lodash._root": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", - "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", - "dev": true - }, - "lodash.at": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.at/-/lodash.at-4.6.0.tgz", - "integrity": "sha1-k83OZk8KGZTqM9181A4jr9EbD/g=", - "optional": true - }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "optional": true }, - "lodash.escape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", - "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", - "dev": true, - "requires": { - "lodash._root": "^3.0.0" - } + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, "lodash.flattendeep": { "version": "4.4.0", @@ -6640,34 +5912,22 @@ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, - "lodash.has": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/lodash.has/-/lodash.has-4.5.2.tgz", - "integrity": "sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI=", - "optional": true - }, "lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, "lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, "lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", @@ -6688,26 +5948,15 @@ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", "dev": true }, "lodash.sortby": { @@ -6717,78 +5966,142 @@ "dev": true }, "lodash.template": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", - "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", "dev": true, "requires": { - "lodash._basecopy": "^3.0.0", - "lodash._basetostring": "^3.0.0", - "lodash._basevalues": "^3.0.0", - "lodash._isiterateecall": "^3.0.0", "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0", - "lodash.keys": "^3.0.0", - "lodash.restparam": "^3.0.0", - "lodash.templatesettings": "^3.0.0" + "lodash.templatesettings": "^4.0.0" } }, "lodash.templatesettings": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", - "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", "dev": true, "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.escape": "^3.0.0" + "lodash._reinterpolate": "^3.0.0" } }, - "lolex": { - "version": "2.7.5", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.7.5.tgz", - "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", - "dev": true + "log-symbols": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", + "dev": true, + "requires": { + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, "long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "optional": true }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "optional": true, "requires": { "yallist": "^3.0.2" } }, + "lru-memoizer": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.1.4.tgz", + "integrity": "sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==", + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "~4.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "lunr": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.6.tgz", - "integrity": "sha512-swStvEyDqQ85MGpABCMBclZcLI/pBIlu8FFDtmX197+oEgKloJ67QnB+Tidh0340HmLMs39c4GrkPY3cmkXp6Q==", + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", - "optional": true, + "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==", "requires": { "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "optional": true - } } }, "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, "make-iterator": { @@ -6800,21 +6113,18 @@ "kind-of": "^6.0.2" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -6825,9 +6135,9 @@ } }, "marked": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz", - "integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-1.2.5.tgz", + "integrity": "sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA==", "dev": true }, "matchdep": { @@ -6865,39 +6175,17 @@ } } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - } - }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true }, "micromatch": { "version": "3.1.10", @@ -6921,21 +6209,22 @@ } }, "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==" + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", + "optional": true }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "requires": { - "mime-db": "~1.37.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -6953,11 +6242,46 @@ } }, "minimist": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.3.tgz", - "integrity": "sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, + "minipass": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", + "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minizlib": { + "version": "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": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "mixin-deep": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", @@ -6974,84 +6298,424 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "^2.0.4" + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mocha": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.2.1.tgz", + "integrity": "sha512-cuLBVfyFfFqbNR0uUKbDGXKGk+UDFe6aR4os78XIrMQpZl/nv7JYHcvP5MFIAb374b2zFXsdgEGwmzMtP0Xg8w==", + "dev": true, + "requires": { + "@ungap/promise-all-settled": "1.1.2", + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.4.3", + "debug": "4.2.0", + "diff": "4.0.2", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "7.1.6", + "growl": "1.10.5", + "he": "1.2.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", + "minimatch": "3.0.4", + "ms": "2.1.2", + "nanoid": "3.1.12", + "serialize-javascript": "5.0.1", + "strip-json-comments": "3.1.1", + "supports-color": "7.2.0", + "which": "2.0.2", + "wide-align": "1.1.3", + "workerpool": "6.0.2", + "yargs": "13.3.2", + "yargs-parser": "13.1.2", + "yargs-unparser": "2.0.0" + }, + "dependencies": { + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "chokidar": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", + "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "dev": true, + "requires": { + "anymatch": "~3.1.1", + "braces": "~3.0.2", + "fsevents": "~2.1.2", + "glob-parent": "~5.1.0", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.5.0" + } + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "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" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "readdirp": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", + "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } } }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "multipipe": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", - "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", "dev": true, "requires": { - "duplexer2": "0.0.2" + "@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": { @@ -7078,9 +6742,16 @@ } }, "nan": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", - "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true, + "optional": true + }, + "nanoid": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.12.tgz", + "integrity": "sha512-1qstj9z5+x491jfiC4Nelk+f8XBad7LN20PmyWINJEMRSf3wcAjAWysw1qaA8z6NSKe2sjq1hRSDpBH5paCb6A==", "dev": true }, "nanomatch": { @@ -7109,15 +6780,9 @@ "dev": true }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true - }, - "nested-error-stacks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", - "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "next-tick": { @@ -7133,87 +6798,60 @@ "dev": true }, "nise": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.4.10.tgz", - "integrity": "sha512-sa0RRbj53dovjc7wombHmVli9ZihXbXCQ2uH3TNm03DyvOSIQbxg+pbqDKrk2oxMK1rtLGVlKxcB9rrc6X5YjA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz", + "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==", "dev": true, "requires": { - "@sinonjs/formatio": "^3.1.0", + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", "@sinonjs/text-encoding": "^0.7.1", "just-extend": "^4.0.2", - "lolex": "^2.3.2", "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - } } }, "nock": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-9.6.1.tgz", - "integrity": "sha512-EDgl/WgNQ0C1BZZlASOQkQdE6tAWXJi8QQlugqzN64JJkvZ7ILijZuG24r4vCC7yOfnm6HKpne5AGExLGCeBWg==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.1.0.tgz", + "integrity": "sha512-3N3DUY8XYrxxzWazQ+nSBpiaJ3q6gcpNh4gXovC/QBxrsvNp4tq+wsLHF6mJ3nrn3lPLn7KCJqKxy/9aD+0fdw==", "dev": true, "requires": { - "chai": "^4.1.2", - "debug": "^3.1.0", - "deep-equal": "^1.0.0", + "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - } + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" } }, + "node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true + }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-forge": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", - "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + }, + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true }, "node-version": { "version": "1.2.0", @@ -7221,16 +6859,33 @@ "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.4.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", - "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "dev": true, "requires": { "hosted-git-info": "^2.1.4", - "is-builtin-module": "^1.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } } }, "normalize-path": { @@ -7243,9 +6898,9 @@ } }, "now-and-later": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz", - "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", "dev": true, "requires": { "once": "^1.3.2" @@ -7278,9 +6933,9 @@ } }, "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { "ansi-styles": "^3.2.1", @@ -7288,109 +6943,305 @@ "supports-color": "^5.3.0" } }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^3.0.0" + } + } + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "nyc": { + "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": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "find-up": { + "version": "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" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "require-main-filename": { + "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-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" } - } - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", - "dev": true - }, - "nyc": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", - "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "caching-transform": "^3.0.2", - "convert-source-map": "^1.6.0", - "cp-file": "^6.2.0", - "find-cache-dir": "^2.1.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.2.3", - "uuid": "^3.3.2", - "yargs": "^13.2.2", - "yargs-parser": "^13.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": false, - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + }, + "string-width": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" + "ansi-regex": "^5.0.0" } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "rimraf": { - "version": "2.6.3", - "resolved": false, - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "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": { - "glob": "^7.1.3" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "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": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.0.tgz", - "integrity": "sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "requires": { "camelcase": "^5.0.0", @@ -7442,22 +7293,23 @@ } } }, - "object-inspect": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", + "object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "optional": true }, - "object-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.2.tgz", - "integrity": "sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==", - "optional": true + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", + "dev": true }, "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true }, "object-visit": { "version": "1.0.1", @@ -7472,6 +7324,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", + "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -7536,44 +7389,18 @@ "mimic-fn": "^2.1.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - } - } - }, "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", "dev": true, "requires": { "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", + "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - }, - "dependencies": { - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - } + "word-wrap": "~1.2.3" } }, "ordered-read-streams": { @@ -7583,46 +7410,8 @@ "dev": true, "requires": { "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -7638,39 +7427,41 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", "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" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } + } + }, + "p-map": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "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": { - "p-limit": "^2.0.0" + "aggregate-error": "^3.0.0" } }, "p-try": { @@ -7679,13 +7470,13 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "package-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", - "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", "dev": true, "requires": { "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", + "hasha": "^5.0.0", "lodash.flattendeep": "^4.4.0", "release-zalgo": "^1.0.0" } @@ -7711,15 +7502,20 @@ } }, "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "error-ex": "^1.2.0" } }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -7755,16 +7551,10 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -7793,21 +7583,31 @@ "dev": true }, "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", "dev": true, "requires": { "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } } }, "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { - "pify": "^3.0.0" + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" } }, "pathval": { @@ -7822,16 +7622,22 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pidtree": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.0.tgz", - "integrity": "sha512-9CT4NFlDcosssyg8KVFltgokyKZIFjoBxw8CTGy+5F38Y1eQWrt8tRayiUOXE+zVKQnYu5BR8JjCtvK3BcnBhg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", "dev": true }, "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, "pinkie": { @@ -7850,22 +7656,29 @@ } }, "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^4.0.0" }, "dependencies": { "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true } } }, @@ -7901,14 +7714,23 @@ }, "pretty-hrtime": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", + "resolved": "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } }, "progress": { "version": "2.0.3", @@ -7923,15 +7745,16 @@ "dev": true }, "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", "dev": true }, "protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", + "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "optional": true, "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -7943,49 +7766,66 @@ "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "10.17.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.11.tgz", - "integrity": "sha512-dNd2pp8qTzzNLAs3O8nH3iU9DG9866KHq9L3ISPB7DOGERZN81nW/5/g/KzMJpCU8jrbCiMRBzV9/sCEdRosig==" - } } }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, "psl": { - "version": "1.1.29", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", - "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", + "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", + "optional": true, "requires": { - "duplexify": "^3.6.0", + "duplexify": "^4.1.1", "inherits": "^2.0.3", - "pump": "^2.0.0" + "pump": "^3.0.0" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "optional": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } } }, "punycode": { @@ -8000,89 +7840,61 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } + "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 }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, - "dependencies": { - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - } + "safe-buffer": "^5.1.0" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" } }, "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "optional": true, + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "requires": { "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, "readdirp": { @@ -8094,38 +7906,6 @@ "graceful-fs": "^4.1.11", "micromatch": "^3.1.10", "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "rechoir": { @@ -8147,64 +7927,10 @@ "safe-regex": "^1.1.0" } }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "optional": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.17.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.0-next.1.tgz", - "integrity": "sha512-7MmGr03N7Rnuid6+wyhD9sHNE2n4tFSwExnU2lQl3lIo2ShXWGePY80zYaoMOmILWv57H0amMjZGHNzzGG70Rw==", - "optional": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimleft": "^2.1.0", - "string.prototype.trimright": "^2.1.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "optional": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "optional": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "optional": true - } - } - }, "regexpp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz", - "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, "release-zalgo": { @@ -8237,36 +7963,6 @@ "through2": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -8298,9 +7994,9 @@ "dev": true }, "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true }, "replace-homedir": { @@ -8314,53 +8010,10 @@ "remove-trailing-separator": "^1.1.0" } }, - "replacestream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/replacestream/-/replacestream-4.0.3.tgz", - "integrity": "sha512-AC0FiLS352pBBiZhd4VXB1Ab/lh0lEgpP+GGvZqbQh8a5cmXVoTe5EX/YeTFArnp4SRGTHh1qCHu9lGs1qG8sA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.3", - "object-assign": "^4.0.1", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "dev": true, "requires": { "aws-sign2": "~0.7.0", @@ -8370,7 +8023,7 @@ "extend": "~3.0.2", "forever-agent": "~0.6.1", "form-data": "~2.3.2", - "har-validator": "~5.1.0", + "har-validator": "~5.1.3", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", @@ -8380,50 +8033,101 @@ "performance-now": "^2.1.0", "qs": "~6.5.2", "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", + "tough-cookie": "~2.5.0", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + } } }, "request-promise": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.2.tgz", - "integrity": "sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/request-promise/-/request-promise-4.2.6.tgz", + "integrity": "sha512-HCHI3DJJUakkOr8fNoCc73E5nU5bqITjOYFMDrKHYOXWXrgD/SBaC7LjwuPymUprRyuF06UK7hd/lMHkmUXglQ==", "dev": true, "requires": { "bluebird": "^3.5.0", - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" + "request-promise-core": "1.1.4", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "request-promise-core": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", + "dev": true, + "requires": { + "lodash": "^4.17.19" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } } }, "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", "dev": true, "requires": { - "lodash": "^4.13.1" + "lodash": "^4.17.15" } }, "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", "dev": true, "requires": { - "request-promise-core": "1.1.2", + "request-promise-core": "1.1.3", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, "dependencies": { - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, "requires": { - "lodash": "^4.17.11" + "psl": "^1.1.28", + "punycode": "^2.1.1" } } } @@ -8431,8 +8135,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, "require-main-filename": { "version": "1.0.1", @@ -8441,12 +8144,12 @@ "dev": true }, "resolve": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", - "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", "dev": true, "requires": { - "path-parse": "^1.0.5" + "path-parse": "^1.0.6" } }, "resolve-dir": { @@ -8504,105 +8207,143 @@ "requires": { "debug": "^4.1.1", "through2": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "optional": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "optional": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "optional": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "through2": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", - "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", - "optional": true, - "requires": { - "readable-stream": "2 || 3" - } - } } }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { - "glob": "^7.0.5" + "glob": "^7.1.3" } }, "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-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": { - "is-promise": "^2.1.0" + "queue-microtask": "^1.2.2" } }, "run-sequence": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-1.2.2.tgz", - "integrity": "sha1-UJWgvr6YczsBQL0I3YDsAw3azes=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-2.2.1.tgz", + "integrity": "sha512-qkzZnQWMZjcKbh3CNly2srtrkaO/2H/SI5f2eliMCapdRD3UhMrwjfOAZJAnZ2H8Ju4aBzFZkBGXUqFs9V0yxw==", "dev": true, "requires": { - "chalk": "*", - "gulp-util": "*" + "chalk": "^1.1.3", + "fancy-log": "^1.3.2", + "plugin-error": "^0.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "arr-diff": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-1.1.0.tgz", + "integrity": "sha1-aHwydYFjWI/vfeezb6vklesaOZo=", + "dev": true, + "requires": { + "arr-flatten": "^1.0.1", + "array-slice": "^0.2.3" + } + }, + "arr-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", + "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", + "dev": true + }, + "array-slice": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", + "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE=", + "dev": true, + "requires": { + "kind-of": "^1.1.0" + } + }, + "kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", + "dev": true + }, + "plugin-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz", + "integrity": "sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4=", + "dev": true, + "requires": { + "ansi-cyan": "^0.1.1", + "ansi-red": "^0.1.1", + "arr-diff": "^1.0.1", + "arr-union": "^2.0.1", + "extend-shallow": "^1.1.2" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "rxjs": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz", - "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.0.tgz", + "integrity": "sha512-3HMA8z/Oz61DUHe+SdOiQyzIf4tOx5oQHmMir7IZEu6TMqCLHT4LRcmNaUS0NwOz8VLvmmBduMsoaUvMaIiqzg==", "dev": true, "requires": { "tslib": "^1.9.0" } }, "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -8615,34 +8356,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "samsam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", - "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", - "dev": true - }, "saxes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.9.tgz", - "integrity": "sha512-FZeKhJglhJHk7eWG5YM0z46VHmI3KJpMBAQm3xa9meDvd+wevB5GuBB0wc0exPInZiBBHqi00DbS8AcvCGCFMw==", - "dev": true, - "requires": { - "xmlchars": "^1.3.1" - } - }, - "scrypt": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/scrypt/-/scrypt-6.0.3.tgz", - "integrity": "sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=", + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", "dev": true, "requires": { - "nan": "^2.0.8" + "xmlchars": "^2.1.1" } }, "semver": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", - "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -8653,6 +8379,15 @@ "sver-compat": "^1.5.0" } }, + "serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8698,21 +8433,15 @@ "dev": true }, "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true }, "shelljs": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.3.tgz", - "integrity": "sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz", + "integrity": "sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==", "dev": true, "requires": { "glob": "^7.0.0", @@ -8721,46 +8450,51 @@ } }, "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, "sinon": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.5.0.tgz", - "integrity": "sha512-trdx+mB0VBBgoYucy6a9L7/jfQOmvGeaKZT4OOJ+lPAtI8623xyGr8wLiE4eojzBS8G9yXbhx42GHUOVLr4X2w==", + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz", + "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==", "dev": true, "requires": { - "@sinonjs/formatio": "^2.0.0", - "diff": "^3.1.0", - "lodash.get": "^4.4.2", - "lolex": "^2.2.0", - "nise": "^1.2.0", - "supports-color": "^5.1.0", - "type-detect": "^4.0.5" + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/samsam": "^5.3.1", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" }, "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true } } }, "sinon-chai": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.14.0.tgz", - "integrity": "sha512-9stIF1utB0ywNHNT7RgiXbdmen8QDCRsrTjw+G9TgKt1Yexjiv8TOWZ6WHsTPz57Yky3DIswZvEqX8fpuHNDtQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.5.0.tgz", + "integrity": "sha512-IifbusYiQBpUxxFJkR3wTU68xzBN0+bxCScEaKMjBvAQERg6FnTTc1F17rseLb1tjmkJ23730AXpFI0c47FgAg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, "slice-ansi": { @@ -8839,6 +8573,12 @@ "requires": { "is-extendable": "^0.1.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -8920,12 +8660,12 @@ "dev": true }, "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "dev": true, "requires": { - "atob": "^2.1.1", + "atob": "^2.1.2", "decode-uri-component": "^0.2.0", "resolve-url": "^0.2.1", "source-map-url": "^0.4.0", @@ -8933,12 +8673,21 @@ } }, "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { - "source-map": "^0.5.6" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } } }, "source-map-url": { @@ -8954,23 +8703,43 @@ "dev": true }, "spawn-wrap": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", - "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", "dev": true, "requires": { - "foreground-child": "^1.5.6", - "mkdirp": "^0.5.0", - "os-homedir": "^1.0.1", - "rimraf": "^2.6.2", + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", "signal-exit": "^3.0.2", - "which": "^1.3.0" + "which": "^2.0.1" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } } }, "spdx-correct": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", - "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -8978,15 +8747,15 @@ } }, "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, "requires": { "spdx-exceptions": "^2.1.0", @@ -8994,9 +8763,9 @@ } }, "spdx-license-ids": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz", - "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", "dev": true }, "split-string": { @@ -9015,9 +8784,9 @@ "dev": true }, "sshpk": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", - "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -9074,6 +8843,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, "requires": { "stubs": "^3.0.0" } @@ -9085,15 +8855,43 @@ "dev": true }, "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "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" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, + "string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -9106,40 +8904,49 @@ } }, "string.prototype.padend": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz", - "integrity": "sha1-86rvfBcZ8XDF6rHDK/eA2W4h8vA=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.0.tgz", + "integrity": "sha512-3aIv8Ffdp8EZj8iLwREGpQaUZiPyrWrpzMBHvkiSW/bK/EGve9np07Vwy7IJ5waydpGXzQZu/F8Oze2/IWkBaA==", "dev": true, "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.4.3", - "function-bind": "^1.0.2" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, - "string.prototype.trimleft": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", - "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "optional": true, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, - "string.prototype.trimright": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", - "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "optional": true, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, "requires": { "define-properties": "^1.1.3", - "function-bind": "^1.1.1" + "es-abstract": "^1.17.5" } }, "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } }, "strip-ansi": { "version": "3.0.1", @@ -9159,28 +8966,28 @@ "is-utf8": "^0.2.0" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - }, "stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", - "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=" + "integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls=", + "optional": true }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + } + } }, "sver-compat": { "version": "1.5.0", @@ -9193,9 +9000,9 @@ } }, "symbol-tree": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", - "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, "table": { @@ -9210,28 +9017,16 @@ "string-width": "^3.0.0" }, "dependencies": { - "ajv": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", - "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, "is-fullwidth-code-point": { @@ -9262,56 +9057,56 @@ } } }, + "tar": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", + "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, "teeny-request": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-5.3.1.tgz", - "integrity": "sha512-hnUeun3xryzv92FbrnprltcdeDfSVaGFBlFPRvKJ2fO/ioQx9N0aSUbbXSfTO+ArRXine1gSWdWFWcgfrggWXw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-7.0.0.tgz", + "integrity": "sha512-kWD3sdGmIix6w7c8ZdVKxWq+3YwVPGWz+Mq0wRZXayEKY/YHb63b8uphfBzcFDmyq8frD9+UTc3wLyOhltRbtg==", "optional": true, "requires": { - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^3.0.0", + "http-proxy-agent": "^4.0.0", + "https-proxy-agent": "^5.0.0", "node-fetch": "^2.2.0", "stream-events": "^1.0.5", - "uuid": "^3.3.2" + "uuid": "^8.0.0" } }, "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - } + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" } }, "text-table": { @@ -9320,16 +9115,10 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, - "textextensions": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/textextensions/-/textextensions-1.0.2.tgz", - "integrity": "sha1-ZUhjk+4fK7A5pgy7oFsLaL2VAdI=", - "dev": true - }, "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { "any-promise": "^1.0.0" @@ -9351,32 +9140,12 @@ "dev": true }, "through2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.0.tgz", - "integrity": "sha512-8B+sevlqP4OiCjonI1Zw03Sf8PuV1eRsYQgLad5eonILOdyeRsY27A/2Ze8IlvlMvq31OH+3fz/styI7Ya62yQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", + "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==", "requires": { - "readable-stream": "2 || 3", - "xtend": "~4.0.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.0.6.tgz", - "integrity": "sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "requires": { - "safe-buffer": "~5.1.0" - } - } + "inherits": "^2.0.4", + "readable-stream": "2 || 3" } }, "through2-filter": { @@ -9389,36 +9158,6 @@ "xtend": "~4.0.0" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9437,6 +9176,12 @@ "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", "dev": true }, + "timsort": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", + "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", + "dev": true + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -9513,36 +9258,6 @@ "through2": "^2.0.3" }, "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -9556,120 +9271,53 @@ } }, "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", "dev": true, "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" } }, "tr46": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "ts-node": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-3.3.0.tgz", - "integrity": "sha1-wTxqMCTjC+EYDdUwOPwgkonUv2k=", - "dev": true, - "requires": { - "arrify": "^1.0.0", - "chalk": "^2.0.0", - "diff": "^3.1.0", - "make-error": "^1.1.1", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "source-map-support": "^0.4.0", - "tsconfig": "^6.0.0", - "v8flags": "^3.0.0", - "yn": "^2.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", - "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "v8flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - } + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" } }, - "tsconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-6.0.0.tgz", - "integrity": "sha1-aw6DdgA9evGGT434+J3QBZ/80DI=", + "ts-node": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", + "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", "dev": true, "requires": { - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true - } + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" } }, "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", + "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "dev": true }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -9686,9 +9334,9 @@ "dev": true }, "type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", "dev": true }, "type-check": { @@ -9701,9 +9349,9 @@ } }, "type-detect": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", - "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, "type-fest": { @@ -9721,82 +9369,79 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "optional": true, "requires": { "is-typedarray": "^1.0.0" } }, "typedoc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.15.0.tgz", - "integrity": "sha512-NOtfq5Tis4EFt+J2ozhVq9RCeUnfEYMFKoU6nCXCXUULJz1UQynOM+yH3TkfZCPLzigbqB0tQYGVlktUWweKlw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.19.2.tgz", + "integrity": "sha512-oDEg1BLEzi1qvgdQXc658EYgJ5qJLVSeZ0hQ57Eq4JXy6Vj2VX4RVo18qYxRWz75ifAaYuYNBUCnbhjd37TfOg==", "dev": true, "requires": { - "@types/minimatch": "3.0.3", - "fs-extra": "^8.1.0", - "handlebars": "^4.1.2", - "highlight.js": "^9.15.8", - "lodash": "^4.17.15", - "marked": "^0.7.0", + "fs-extra": "^9.0.1", + "handlebars": "^4.7.6", + "highlight.js": "^10.2.0", + "lodash": "^4.17.20", + "lunr": "^2.3.9", + "marked": "^1.1.1", "minimatch": "^3.0.0", "progress": "^2.0.3", - "shelljs": "^0.8.3", - "typedoc-default-themes": "^0.6.0", - "typescript": "3.5.x" + "semver": "^7.3.2", + "shelljs": "^0.8.4", + "typedoc-default-themes": "^0.11.4" }, "dependencies": { - "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true } } }, "typedoc-default-themes": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.6.0.tgz", - "integrity": "sha512-MdTROOojxod78CEv22rIA69o7crMPLnVZPefuDLt/WepXqJwgiSu8Xxq+H36x0Jj3YGc7lOglI2vPJ2GhoOybw==", - "dev": true, - "requires": { - "backbone": "^1.4.0", - "jquery": "^3.4.1", - "lunr": "^2.3.6", - "underscore": "^1.9.1" - } + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/typedoc-default-themes/-/typedoc-default-themes-0.11.4.tgz", + "integrity": "sha512-Y4Lf+qIb9NTydrexlazAM46SSLrmrQRqWiD52593g53SsmUFioAsMWt8m834J6qsp+7wHRjxCXSZeiiW5cMUdw==", + "dev": true }, "typescript": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", - "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz", + "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.1.tgz", + "integrity": "sha512-o8lHP20KjIiQe5b/67Rh68xEGRrc2SRsCuuoYclXXoC74AfSRGblU1HKzJWH3HxPZ+Ort85fWHpSX7KwBUC9CQ==", "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } + "optional": true }, "unc-path-regex": { "version": "0.1.2", @@ -9804,12 +9449,6 @@ "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", "dev": true }, - "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==", - "dev": true - }, "undertaker": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz", @@ -9907,19 +9546,13 @@ "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true } } }, "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", "dev": true }, "uri-js": { @@ -9949,20 +9582,21 @@ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true }, "v8-compile-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", - "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", + "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "dev": true }, "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "dev": true, "requires": { "homedir-polyfill": "^1.0.1" @@ -9978,6 +9612,12 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz", + "integrity": "sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA==", + "dev": true + }, "value-or-function": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", @@ -9996,14 +9636,17 @@ } }, "vinyl": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", - "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", + "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", "dev": true, "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" } }, "vinyl-fs": { @@ -10031,52 +9674,25 @@ "vinyl-sourcemap": "^1.1.0" }, "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "safe-buffer": "~5.1.0" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" } }, "through2": { @@ -10088,20 +9704,6 @@ "readable-stream": "~2.3.6", "xtend": "~4.0.1" } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } } } }, @@ -10118,49 +9720,15 @@ "now-and-later": "^2.0.0", "remove-bom-buffer": "^3.0.0", "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } } }, "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", "dev": true, "requires": { - "browser-process-hrtime": "^0.1.2" + "browser-process-hrtime": "^1.0.0" } }, "w3c-xmlserializer": { @@ -10174,12 +9742,6 @@ "xml-name-validator": "^3.0.0" } }, - "walkdir": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/walkdir/-/walkdir-0.4.1.tgz", - "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==", - "optional": true - }, "webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", @@ -10187,11 +9749,11 @@ "dev": true }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "requires": { - "http-parser-js": ">=0.4.0 <0.4.11", + "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } @@ -10217,9 +9779,9 @@ "dev": true }, "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", "dev": true, "requires": { "lodash.sortby": "^4.7.0", @@ -10242,6 +9804,15 @@ "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", "dev": true }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -10249,9 +9820,15 @@ "dev": true }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "workerpool": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.0.2.tgz", + "integrity": "sha512-DSNyvOpFKrNusaaUwk+ej6cBj1bmhLcBfj80elGk+ZIo5JSkq+unB1dLKEOcNfJDZgjGICfhQ0Q5TbP0PvF4+Q==", "dev": true }, "wrap-ansi": { @@ -10279,29 +9856,27 @@ } }, "write-file-atomic": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.2.tgz", - "integrity": "sha512-s0b6vB3xIVRLWywa6X9TOMA7k9zio0TMOsl9ZnDkliA/cfJlpHXAscj0gbHVJiTdIuAYpIyqS5GW91fqm6gG5g==", - "dev": true, + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", "requires": { - "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, "ws": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.0.0.tgz", - "integrity": "sha512-cknCal4k0EAOrh1SHHPPWWh4qm93g1IuGGGwBjWkXmCG7LsDtL8w9w+YVfaF+KSVwiHQKDIMsSLBVftKf9d1pg==", - "dev": true, - "requires": { - "async-limiter": "^1.0.0" - } + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "dev": true }, "xdg-basedir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "optional": true }, "xml-name-validator": { "version": "3.0.0", @@ -10310,9 +9885,9 @@ "dev": true }, "xmlchars": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-1.3.1.tgz", - "integrity": "sha512-tGkGJkN8XqCod7OT+EvGYK5Z4SfDQGD30zAa58OcnAa0RRWgzUEK72tkXhsX1FZd+rgnhRxFtmO+ihkp8LHSkw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, "xmlhttprequest": { @@ -10322,73 +9897,53 @@ "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": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "optional": true }, "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz", + "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==", "dev": true, "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "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": { - "locate-path": "^3.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" } }, "get-caller-file": { @@ -10397,121 +9952,110 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" } }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, "yargs-parser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.0.tgz", - "integrity": "sha512-Yq+32PrijHRri0vVKQEm+ys8mbqWjLiwQkMFNXEENutzLPP0bE4Lcd4iA3OQY5HF+GD3xXxf0MEHb8E4/SA3AA==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "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 } } }, "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "version": "5.0.0-security.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", + "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + } + }, + "yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, "requires": { - "camelcase": "^3.0.0" + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" }, "dependencies": { "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true } } }, "yn": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", - "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "dev": true + }, + "z-schema": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz", + "integrity": "sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw==", + "dev": true, + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.0.0", + "lodash.isequal": "^4.0.0", + "validator": "^8.0.0" + } } } } diff --git a/package.json b/package.json index d8057c1056..01ee014918 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "firebase-admin", - "version": "8.13.0", + "version": "9.11.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", "homepage": "https://firebase.google.com/", "engines": { - "node": "^8.13.0 || >=10.10.0" + "node": ">=10.13.0" }, "scripts": { "build": "gulp build", @@ -20,7 +20,9 @@ "test:coverage": "nyc npm run test:unit", "lint:src": "eslint src/ --ext .ts", "lint:test": "eslint test/ --ext .ts", - "apidocs": "node docgen/generate-docs.js --api node" + "apidocs": "node docgen/generate-docs.js --api node", + "api-extractor": "api-extractor run", + "api-extractor:local": "api-extractor run --local" }, "nyc": { "extension": [ @@ -54,66 +56,67 @@ ], "types": "./lib/index.d.ts", "dependencies": { - "@firebase/database": "^0.6.0", - "@types/node": "^8.10.59", + "@firebase/database": "^0.10.0", + "@firebase/database-types": "^0.7.2", + "@types/node": ">=12.12.47", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", - "node-forge": "^0.7.6" + "jwks-rsa": "^2.0.2", + "node-forge": "^0.10.0" }, "optionalDependencies": { - "@google-cloud/firestore": "^3.0.0", - "@google-cloud/storage": "^4.1.2" + "@google-cloud/firestore": "^4.5.0", + "@google-cloud/storage": "^5.3.0" }, "devDependencies": { - "@firebase/app": "^0.6.1", - "@firebase/auth": "^0.13.3", - "@firebase/auth-types": "^0.9.3", + "@firebase/app": "^0.6.21", + "@firebase/auth": "^0.16.5", + "@firebase/auth-types": "^0.10.3", + "@microsoft/api-extractor": "^7.11.2", "@types/bcrypt": "^2.0.0", - "@types/chai": "^3.4.34", - "@types/chai-as-promised": "0.0.29", + "@types/chai": "^4.0.0", + "@types/chai-as-promised": "^7.1.0", "@types/firebase-token-generator": "^2.0.28", - "@types/jsonwebtoken": "^7.2.8", + "@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.104", "@types/minimist": "^1.2.0", - "@types/mocha": "^2.2.48", - "@types/nock": "^9.1.0", + "@types/mocha": "^8.2.2", + "@types/nock": "^11.1.0", "@types/request": "^2.47.0", "@types/request-promise": "^4.1.41", - "@types/scrypt": "^6.0.0", - "@types/sinon": "^4.1.3", - "@types/sinon-chai": "^2.7.27", + "@types/sinon": "^10.0.2", + "@types/sinon-chai": "^3.0.0", "@typescript-eslint/eslint-plugin": "^2.20.0", "@typescript-eslint/parser": "^2.20.0", - "bcrypt": "^3.0.0", - "chai": "^3.5.0", - "chai-as-promised": "^6.0.0", - "chalk": "^1.1.3", + "bcrypt": "^5.0.0", + "chai": "^4.2.0", + "chai-as-promised": "^7.0.0", + "chalk": "^4.1.1", "child-process-promise": "^2.2.1", - "del": "^2.2.1", + "del": "^6.0.0", "eslint": "^6.8.0", "firebase-token-generator": "^2.0.0", "gulp": "^4.0.2", - "gulp-header": "^1.8.8", - "gulp-replace": "^0.5.4", + "gulp-filter": "^7.0.0", + "gulp-header": "^2.0.9", "gulp-typescript": "^5.0.1", "http-message-parser": "^0.0.34", "jsdom": "^15.0.0", "lodash": "^4.17.15", "minimist": "^1.2.0", - "mocha": "^5.2.0", + "mocha": "^8.0.0", "mz": "^2.7.0", - "nock": "^9.6.0", + "nock": "^13.0.0", "npm-run-all": "^4.1.5", - "nyc": "^14.1.0", + "nyc": "^15.1.0", "request": "^2.75.0", "request-promise": "^4.1.1", - "run-sequence": "^1.1.5", - "scrypt": "^6.0.3", - "sinon": "^4.5.0", - "sinon-chai": "^2.14.0", - "ts-node": "^3.3.0", - "typedoc": "^0.15.0", + "run-sequence": "^2.2.1", + "sinon": "^9.0.0", + "sinon-chai": "^3.0.0", + "ts-node": "^9.0.0", + "typedoc": "^0.19.2", "typescript": "^3.7.3", - "yargs": "^13.2.2" + "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..8d25e23cf7 --- /dev/null +++ b/src/app-check/app-check-api-client-internal.ts @@ -0,0 +1,228 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { appCheck } from './index'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse +} from '../utils/api-request'; +import { FirebaseApp } from '../firebase-app'; +import { PrefixedFirebaseError } from '../utils/error'; + +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; + +import AppCheckToken = appCheck.AppCheckToken; + +// App Check backend constants +const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}/apps/{appId}:exchangeCustomToken'; + +const FIREBASE_APP_CHECK_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; + +/** + * Class that facilitates sending requests to the Firebase App Check backend API. + * + * @internal + */ +export class AppCheckApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + + constructor(private readonly app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'First argument passed to admin.appCheck() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(app); + } + + /** + * Exchange a signed custom token to App Check token + * + * @param customToken The custom token to be exchanged. + * @param appId The mobile App ID. + * @return A promise that fulfills with a `AppCheckToken`. + */ + public exchangeToken(customToken: string, appId: string): Promise { + if (!validator.isNonEmptyString(appId)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`appId` must be a non-empty string.'); + } + if (!validator.isNonEmptyString(customToken)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + '`customToken` must be a non-empty string.'); + } + return this.getUrl(appId) + .then((url) => { + const request: HttpRequestConfig = { + method: 'POST', + url, + headers: FIREBASE_APP_CHECK_CONFIG_HEADERS, + data: { customToken } + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toAppCheckToken(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private getUrl(appId: string): Promise { + return this.getProjectId() + .then((projectId) => { + const urlParams = { + projectId, + appId, + }; + const baseUrl = utils.formatString(FIREBASE_APP_CHECK_V1_API_URL_FORMAT, urlParams); + return utils.formatString(baseUrl); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAppCheckError( + 'unknown-error', + 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + } + this.projectId = projectId; + return projectId; + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseAppCheckError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: AppCheckErrorCode = 'unknown-error'; + if (error.status && error.status in APP_CHECK_ERROR_CODE_MAPPING) { + code = APP_CHECK_ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseAppCheckError(code, message); + } + + /** + * Creates an AppCheckToken from the API response. + * + * @param resp API response object. + * @return An AppCheckToken instance. + */ + private toAppCheckToken(resp: HttpResponse): AppCheckToken { + const token = resp.data.attestationToken; + // `ttl` is a string with the suffix "s" preceded by the number of seconds, + // with nanoseconds expressed as fractional seconds. + const ttlMillis = this.stringToMilliseconds(resp.data.ttl); + return { + token, + ttlMillis + } + } + + /** + * Converts a duration string with the suffix `s` to milliseconds. + * + * @param duration The duration as a string with the suffix "s" preceded by the + * number of seconds, with fractional seconds. For example, 3 seconds with 0 nanoseconds + * is expressed as "3s", while 3 seconds and 1 nanosecond is expressed as "3.000000001s", + * and 3 seconds and 1 microsecond is expressed as "3.000001s". + * + * @return The duration in milliseconds. + */ + private stringToMilliseconds(duration: string): number { + if (!validator.isNonEmptyString(duration) || !duration.endsWith('s')) { + throw new FirebaseAppCheckError( + 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + } + const seconds = duration.slice(0, -1); + return Math.floor(Number(seconds) * 1000); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +export const APP_CHECK_ERROR_CODE_MAPPING: { [key: string]: AppCheckErrorCode } = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +export type AppCheckErrorCode = + 'aborted' + | 'invalid-argument' + | 'invalid-credential' + | 'internal-error' + | 'permission-denied' + | 'unauthenticated' + | 'not-found' + | 'app-check-token-expired' + | 'unknown-error'; + +/** + * Firebase App Check error code structure. This extends PrefixedFirebaseError. + * + * @param {AppCheckErrorCode} code The error code. + * @param {string} message The error message. + * @constructor + */ +export class FirebaseAppCheckError extends PrefixedFirebaseError { + constructor(code: AppCheckErrorCode, message: string) { + super('app-check', code, message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebaseAppCheckError.prototype; + } +} diff --git a/src/app-check/app-check.ts b/src/app-check/app-check.ts new file mode 100644 index 0000000000..175d023419 --- /dev/null +++ b/src/app-check/app-check.ts @@ -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. + */ + +import { FirebaseApp } from '../firebase-app'; +import { appCheck } from './index'; +import { AppCheckApiClient } from './app-check-api-client-internal'; +import { + appCheckErrorFromCryptoSignerError, AppCheckTokenGenerator +} from './token-generator'; +import { AppCheckTokenVerifier } from './token-verifier'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; + +import AppCheckInterface = appCheck.AppCheck; +import AppCheckToken = appCheck.AppCheckToken; +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; +import VerifyAppCheckTokenResponse = appCheck.VerifyAppCheckTokenResponse; + +/** + * AppCheck service bound to the provided app. + */ +export class AppCheck implements AppCheckInterface { + + private readonly client: AppCheckApiClient; + private readonly tokenGenerator: AppCheckTokenGenerator; + private readonly appCheckTokenVerifier: AppCheckTokenVerifier; + + /** + * @param app The app for this AppCheck service. + * @constructor + */ + constructor(readonly app: FirebaseApp) { + this.client = new AppCheckApiClient(app); + try { + this.tokenGenerator = new AppCheckTokenGenerator(cryptoSignerFromApp(app)); + } catch (err) { + throw appCheckErrorFromCryptoSignerError(err); + } + this.appCheckTokenVerifier = new AppCheckTokenVerifier(app); + } + + /** + * Creates a new {@link appCheck.AppCheckToken `AppCheckToken`} that can be sent + * back to a client. + * + * @param appId The app ID to use as the JWT app_id. + * @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 an App Check token. + * + * @param appCheckToken The App Check token to verify. + * + * @return A promise that fulfills with a `VerifyAppCheckTokenResponse` on successful + * verification. + */ + public verifyToken(appCheckToken: string): Promise { + return this.appCheckTokenVerifier.verifyToken(appCheckToken) + .then((decodedToken) => { + return { + appId: decodedToken.app_id, + token: decodedToken, + }; + }); + } +} diff --git a/src/app-check/index.ts b/src/app-check/index.ts new file mode 100644 index 0000000000..295f49e4be --- /dev/null +++ b/src/app-check/index.ts @@ -0,0 +1,176 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link appCheck.AppCheck `AppCheck`} service for the + * default app or a given app. + * + * You can call `admin.appCheck()` with no arguments to access the default + * app's {@link appCheck.AppCheck `AppCheck`} service or as + * `admin.appCheck(app)` to access the + * {@link appCheck.AppCheck `AppCheck`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the `AppCheck` service for the default app + * var defaultAppCheck = admin.appCheck(); + * ``` + * + * @example + * ```javascript + * // Get the `AppCheck` service for a given app + * var otherAppCheck = admin.appCheck(otherApp); + * ``` + * + * @param app Optional app for which to return the `AppCheck` service. + * If not provided, the default `AppCheck` service is returned. + * + * @return The default `AppCheck` service if no + * app is provided, or the `AppCheck` service associated with the provided + * app. + */ +export declare function appCheck(app?: app.App): appCheck.AppCheck; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace appCheck { + /** + * The Firebase `AppCheck` service interface. + */ + export interface AppCheck { + app: app.App; + + /** + * Creates a new {@link appCheck.AppCheckToken `AppCheckToken`} that can be sent + * back to a client. + * + * @param appId The App ID of the Firebase App the token belongs to. + * @param options Optional options object when creating a new App Check Token. + * + * @returns A promise that fulfills with a `AppCheckToken`. + */ + createToken(appId: string, options?: AppCheckTokenOptions): Promise; + + /** + * Verifies a Firebase App Check token (JWT). If the token is valid, the promise is + * fulfilled with the token's decoded claims; otherwise, the promise is + * rejected. + * + * @param appCheckToken The App Check token to verify. + * + * @return A promise fulfilled with the + * token's decoded claims if the App Check token is valid; otherwise, a rejected + * promise. + */ + verifyToken(appCheckToken: string): Promise; + } + + /** + * Interface representing an App Check token. + */ + export interface AppCheckToken { + /** + * The Firebase App Check token. + */ + token: string; + + /** + * The time-to-live duration of the token in milliseconds. + */ + ttlMillis: number; + } + + /** + * Interface representing App Check token options. + */ + export interface AppCheckTokenOptions { + /** + * The length of time, in milliseconds, for which the App Check token will + * be valid. This value must be between 30 minutes and 7 days, inclusive. + */ + ttlMillis?: number; + } + + /** + * Interface representing a decoded Firebase App Check token, returned from the + * {@link appCheck.AppCheck.verifyToken `verifyToken()`} method. + */ + export interface DecodedAppCheckToken { + /** + * The issuer identifier for the issuer of the response. + * + * This value is a URL with the format + * `https://firebaseappcheck.googleapis.com/`, where `` is the + * same project number specified in the [`aud`](#aud) property. + */ + iss: string; + + /** + * The Firebase App ID corresponding to the app the token belonged to. + * + * As a convenience, this value is copied over to the [`app_id`](#app_id) property. + */ + sub: string; + + /** + * The audience for which this token is intended. + * + * This value is a JSON array of two strings, the first is the project number of your + * Firebase project, and the second is the project ID of the same project. + */ + aud: string[]; + + /** + * The App Check token's expiration time, in seconds since the Unix epoch. That is, the + * time at which this App Check token expires and should no longer be considered valid. + */ + exp: number; + + /** + * The App Check token's issued-at time, in seconds since the Unix epoch. That is, the + * time at which this App Check token was issued and should start to be considered + * valid. + */ + iat: number; + + /** + * The App ID corresponding to the App the App Check token belonged to. + * + * This value is not actually one of the JWT token claims. It is added as a + * convenience, and is set as the value of the [`sub`](#sub) property. + */ + app_id: string; + [key: string]: any; + } + + /** + * Interface representing a verified App Check token response. + */ + export interface VerifyAppCheckTokenResponse { + /** + * The App ID corresponding to the App the App Check token belonged to. + */ + appId: string; + + /** + * The decoded Firebase App Check token. + */ + token: appCheck.DecodedAppCheckToken; + } +} diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts new file mode 100644 index 0000000000..86745b793c --- /dev/null +++ b/src/app-check/token-generator.ts @@ -0,0 +1,185 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { appCheck } from './index'; + +import * 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 { HttpError } from '../utils/api-request'; + +import AppCheckTokenOptions = appCheck.AppCheckTokenOptions; + +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.v1beta.TokenExchangeService'; + +/** + * Class for generating Firebase App Check tokens. + * + * @internal + */ +export class AppCheckTokenGenerator { + + private readonly signer: CryptoSigner; + + /** + * The AppCheckTokenGenerator class constructor. + * + * @param signer The CryptoSigner instance for this token generator. + * @constructor + */ + constructor(signer: CryptoSigner) { + if (!validator.isNonNullObject(signer)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'INTERNAL ASSERT: Must provide a CryptoSigner to use AppCheckTokenGenerator.'); + } + this.signer = signer; + } + + /** + * Creates a new custom token that can be exchanged to an App Check token. + * + * @param appId The Application ID to use for the generated token. + * + * @return A Promise fulfilled with a custom token signed with a service account key + * that can be exchanged to an App Check token. + */ + public createCustomToken(appId: string, 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, + // eslint-disable-next-line @typescript-eslint/camelcase + 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 + * @return A Firebase App Check error that can be returned to the user. + */ +export function appCheckErrorFromCryptoSignerError(err: Error): Error { + if (!(err instanceof CryptoSignerError)) { + return err; + } + if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) { + const httpError = err.cause as HttpError + const errorResponse = httpError.response.data; + if (errorResponse?.error) { + const status = errorResponse.error.status; + const description = errorResponse.error.message || JSON.stringify(httpError.response); + + let code: AppCheckErrorCode = 'unknown-error'; + if (status && status in APP_CHECK_ERROR_CODE_MAPPING) { + code = APP_CHECK_ERROR_CODE_MAPPING[status]; + } + return new FirebaseAppCheckError(code, + `Error returned from server while 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..318a1fd10b --- /dev/null +++ b/src/app-check/token-verifier.ts @@ -0,0 +1,165 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { appCheck } from '.'; +import * as validator from '../utils/validator'; +import * as util from '../utils/index'; +import { FirebaseAppCheckError } from './app-check-api-client-internal'; +import { FirebaseApp } from '../firebase-app'; +import { + ALGORITHM_RS256, DecodedToken, decodeJwt, JwtError, + JwtErrorCode, PublicKeySignatureVerifier, SignatureVerifier +} from '../utils/jwt'; + +import DecodedAppCheckToken = appCheck.DecodedAppCheckToken; + +const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'; +const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1beta/jwks'; + +/** + * Class for verifying Firebase App Check tokens. + * + * @internal + */ +export class AppCheckTokenVerifier { + private readonly signatureVerifier: SignatureVerifier; + + constructor(private readonly app: FirebaseApp) { + this.signatureVerifier = PublicKeySignatureVerifier.withJwksUrl(JWKS_URL); + } + + /** + * Verifies the format and signature of a Firebase App Check token. + * + * @param token The Firebase Auth JWT token to verify. + * @return A promise fulfilled with the decoded claims of the Firebase App Check token. + */ + public verifyToken(token: string): Promise { + if (!validator.isString(token)) { + throw new FirebaseAppCheckError( + 'invalid-argument', + 'App check token must be a non-null string.', + ); + } + + return this.ensureProjectId() + .then((projectId) => { + return this.decodeAndVerify(token, projectId); + }) + .then((decoded) => { + const decodedAppCheckToken = decoded.payload as DecodedAppCheckToken; + // eslint-disable-next-line @typescript-eslint/camelcase + decodedAppCheckToken.app_id = decodedAppCheckToken.sub; + return decodedAppCheckToken; + }); + } + + private ensureProjectId(): Promise { + return util.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAppCheckError( + 'invalid-credential', + 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + 'GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.' + ); + } + return projectId; + }) + } + + private decodeAndVerify(token: string, projectId: string): Promise { + return this.safeDecode(token) + .then((decodedToken) => { + this.verifyContent(decodedToken, projectId); + return this.verifySignature(token) + .then(() => decodedToken); + }); + } + + private safeDecode(jwtToken: string): Promise { + return decodeJwt(jwtToken) + .catch(() => { + const errorMessage = 'Decoding App Check token failed. Make sure you passed ' + + 'the entire string JWT which represents the Firebase App Check token.'; + throw new FirebaseAppCheckError('invalid-argument', errorMessage); + }); + } + + /** + * Verifies the content of a Firebase App Check JWT. + * + * @param fullDecodedToken The decoded JWT. + * @param projectId The Firebase Project Id. + */ + private verifyContent(fullDecodedToken: DecodedToken, projectId: string | null): void { + const header = fullDecodedToken.header; + const payload = fullDecodedToken.payload; + + const projectIdMatchMessage = ' Make sure the App Check token comes from the same ' + + 'Firebase project as the service account used to authenticate this SDK.'; + const scopedProjectId = `projects/${projectId}`; + + let errorMessage: string | undefined; + if (header.alg !== ALGORITHM_RS256) { + errorMessage = 'The provided App Check token has incorrect algorithm. Expected "' + + ALGORITHM_RS256 + '" but got ' + '"' + header.alg + '".'; + } else if (!validator.isNonEmptyArray(payload.aud) || !payload.aud.includes(scopedProjectId)) { + errorMessage = 'The provided App Check token has incorrect "aud" (audience) claim. Expected "' + + scopedProjectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage; + } else if (typeof payload.iss !== 'string' || !payload.iss.startsWith(APP_CHECK_ISSUER)) { + errorMessage = 'The provided App Check token has incorrect "iss" (issuer) claim.'; + } else if (typeof payload.sub !== 'string') { + errorMessage = 'The provided App Check token has no "sub" (subject) claim.'; + } else if (payload.sub === '') { + errorMessage = 'The provided App Check token has an empty string "sub" (subject) claim.'; + } + if (errorMessage) { + throw new FirebaseAppCheckError('invalid-argument', errorMessage); + } + } + + private verifySignature(jwtToken: string): + Promise { + return this.signatureVerifier.verify(jwtToken) + .catch((error: JwtError) => { + throw this.mapJwtErrorToAppCheckError(error); + }); + } + + /** + * Maps JwtError to FirebaseAppCheckError + * + * @param error JwtError to be mapped. + * @returns FirebaseAppCheckError instance. + */ + private mapJwtErrorToAppCheckError(error: JwtError): FirebaseAppCheckError { + if (error.code === JwtErrorCode.TOKEN_EXPIRED) { + const errorMessage = 'The provided App Check token has expired. Get a fresh App Check token' + + ' from your client app and try again.' + return new FirebaseAppCheckError('app-check-token-expired', errorMessage); + } else if (error.code === JwtErrorCode.INVALID_SIGNATURE) { + const errorMessage = 'The provided App Check token has invalid signature.'; + return new FirebaseAppCheckError('invalid-argument', errorMessage); + } else if (error.code === JwtErrorCode.NO_MATCHING_KID) { + const errorMessage = 'The provided App Check token has "kid" claim which does not ' + + 'correspond to a known public key. Most likely the provided App Check token ' + + 'is expired, so get a fresh token from your client app and try again.'; + return new FirebaseAppCheckError('invalid-argument', errorMessage); + } + return new FirebaseAppCheckError('invalid-argument', error.message); + } +} diff --git a/src/auth/action-code-settings-builder.ts b/src/auth/action-code-settings-builder.ts index 159e78af48..14c212b6fe 100644 --- a/src/auth/action-code-settings-builder.ts +++ b/src/auth/action-code-settings-builder.ts @@ -15,22 +15,10 @@ */ import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; -/** Defines the ActionCodeSettings interface. */ -export interface ActionCodeSettings { - url: string; - handleCodeInApp?: boolean; - iOS?: { - bundleId: string; - }; - android?: { - packageName: string; - installApp?: boolean; - minimumVersion?: string; - }; - dynamicLinkDomain?: string; -} +import ActionCodeSettings = auth.ActionCodeSettings; /** Defines the email action code server request. */ interface EmailActionCodeRequest { diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts old mode 100755 new mode 100644 index faa56a917b..e7f429f811 --- 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,35 +17,48 @@ import * as validator from '../utils/validator'; -import {deepCopy, deepExtend} from '../utils/deep-copy'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; import { - UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, - isProviderIdentifier, UidIdentifier, EmailIdentifier, PhoneIdentifier, - ProviderIdentifier, + isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier } from './identifier'; -import {FirebaseApp} from '../firebase-app'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; -import {CreateRequest, UpdateRequest} from './user-record'; import { - UserImportBuilder, UserImportOptions, UserImportRecord, - UserImportResult, AuthFactorInfo, convertMultiFactorInfoToServerFormat, + UserImportBuilder, AuthFactorInfo, convertMultiFactorInfoToServerFormat, } from './user-import-builder'; import * as utils from '../utils/index'; -import {ActionCodeSettings, ActionCodeSettingsBuilder} from './action-code-settings-builder'; +import { ActionCodeSettingsBuilder } from './action-code-settings-builder'; import { SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, - OIDCConfigServerRequest, SAMLConfigServerRequest, AuthProviderConfig, - OIDCUpdateAuthProviderRequest, SAMLUpdateAuthProviderRequest, + OIDCConfigServerRequest, SAMLConfigServerRequest, } from './auth-config'; -import {Tenant, TenantOptions, TenantServerResponse} from './tenant'; - +import { Tenant, TenantServerResponse } from './tenant'; +import { auth } from './index'; + +import CreateRequest = auth.CreateRequest; +import UpdateRequest = auth.UpdateRequest; +import UserIdentifier = auth.UserIdentifier; +import UidIdentifier = auth.UidIdentifier; +import EmailIdentifier = auth.EmailIdentifier; +import PhoneIdentifier = auth.PhoneIdentifier; +import ProviderIdentifier = auth.ProviderIdentifier; +import UserImportOptions = auth.UserImportOptions; +import UserImportRecord = auth.UserImportRecord; +import UserImportResult = auth.UserImportResult; +import ActionCodeSettings = auth.ActionCodeSettings; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** Firebase Auth request header. */ const FIREBASE_AUTH_HEADER = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${utils.getSdkVersion()}`, }; /** Firebase Auth request timeout duration in milliseconds. */ const FIREBASE_AUTH_TIMEOUT = 25000; @@ -89,10 +103,19 @@ const MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE = 100; const FIREBASE_AUTH_BASE_URL_FORMAT = 'https://identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; +/** Firebase Auth base URlLformat when using the auth emultor. */ +const FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT = + 'http://{host}/identitytoolkit.googleapis.com/{version}/projects/{projectId}{api}'; + /** The Firebase Auth backend multi-tenancy base URL format. */ const FIREBASE_AUTH_TENANT_URL_FORMAT = FIREBASE_AUTH_BASE_URL_FORMAT.replace( 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); +/** Firebase Auth base URL format when using the auth emultor with multi-tenancy. */ +const FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT = FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT.replace( + 'projects/{projectId}', 'projects/{projectId}/tenants/{tenantId}'); + + /** Maximum allowed number of tenants to download at one time. */ const MAX_LIST_TENANT_PAGE_SIZE = 1000; @@ -121,7 +144,13 @@ class AuthResourceUrlBuilder { * @constructor */ constructor(protected app: FirebaseApp, protected version: string = 'v1') { - this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; + if (useEmulator()) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_BASE_URL_FORMAT, { + host: emulatorHost() + }); + } else { + this.urlFormat = FIREBASE_AUTH_BASE_URL_FORMAT; + } } /** @@ -181,7 +210,13 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { */ constructor(protected app: FirebaseApp, protected version: string, protected tenantId: string) { super(app, version); - this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + if (useEmulator()) { + this.urlFormat = utils.formatString(FIREBASE_AUTH_EMULATOR_TENANT_URL_FORMAT, { + host: emulatorHost() + }); + } else { + this.urlFormat = FIREBASE_AUTH_TENANT_URL_FORMAT; + } } /** @@ -195,11 +230,26 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { public getUrl(api?: string, params?: object): Promise { return super.getUrl(api, params) .then((url) => { - return utils.formatString(url, {tenantId: this.tenantId}); + return utils.formatString(url, { tenantId: this.tenantId }); }); } } +/** + * 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 @@ -207,9 +257,8 @@ class TenantAwareAuthResourceUrlBuilder extends AuthResourceUrlBuilder { * an error is thrown. * * @param request The AuthFactorInfo request object. - * @param writeOperationType The write operation type. */ -function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: WriteOperationType): void { +function validateAuthFactorInfo(request: AuthFactorInfo): void { const validKeys = { mfaEnrollmentId: true, displayName: true, @@ -225,12 +274,12 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri // No enrollment ID is available for signupNewUser. Use another identifier. const authFactorInfoIdentifier = request.mfaEnrollmentId || request.phoneInfo || JSON.stringify(request); - const uidRequired = writeOperationType !== WriteOperationType.Create; - if ((typeof request.mfaEnrollmentId !== 'undefined' || uidRequired) && + // Enrollment uid may or may not be specified for update operations. + if (typeof request.mfaEnrollmentId !== 'undefined' && !validator.isNonEmptyString(request.mfaEnrollmentId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ); } if (typeof request.displayName !== 'undefined' && @@ -246,7 +295,7 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${authFactorInfoIdentifier}" must be a valid ` + - `UTC date string.`); + 'UTC date string.'); } // Validate required fields depending on second factor type. if (typeof request.phoneInfo !== 'undefined') { @@ -255,14 +304,14 @@ function validateAuthFactorInfo(request: AuthFactorInfo, writeOperationType: Wri throw new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, `The second factor "phoneNumber" for "${authFactorInfoIdentifier}" must be a non-empty ` + - `E.164 standard compliant identifier string.`); + 'E.164 standard compliant identifier string.'); } } else { // Invalid second factor. For example, a phone second factor may have been provided without // a phone number. A TOTP based second factor may require a secret key, etc. throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLED_FACTORS, - `MFAInfo object provided is invalid.`); + 'MFAInfo object provided is invalid.'); } } @@ -353,6 +402,8 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat phoneNumber: true, customAttributes: true, validSince: true, + // Pass linkProviderUserInfo only for updates (i.e. not for uploads.) + linkProviderUserInfo: !uploadAccountRequest, // Pass tenantId only for uploadAccount requests. tenantId: uploadAccountRequest, passwordHash: uploadAccountRequest, @@ -501,6 +552,12 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat validateProviderUserInfo(providerUserInfoEntry); }); } + + // linkProviderUserInfo must be a (single) UserProvider value. + if (typeof request.linkProviderUserInfo !== 'undefined') { + validateProviderUserInfo(request.linkProviderUserInfo); + } + // mfaInfo is used for importUsers. // mfa.enrollments is used for setAccountInfo. // enrollments has to be an array of valid AuthFactorInfo requests. @@ -515,7 +572,7 @@ function validateCreateEditRequest(request: any, writeOperationType: WriteOperat throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ENROLLED_FACTORS); } enrollments.forEach((authFactorInfoEntry: AuthFactorInfo) => { - validateAuthFactorInfo(authFactorInfoEntry, writeOperationType); + validateAuthFactorInfo(authFactorInfoEntry); }); } } @@ -565,7 +622,7 @@ export const FIREBASE_AUTH_DOWNLOAD_ACCOUNT = new ApiSettings('/accounts:batchGe request.maxResults > MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_DOWNLOAD_ACCOUNT_PAGE_SIZE}.`, ); } @@ -593,7 +650,7 @@ export const FIREBASE_AUTH_GET_ACCOUNT_INFO = new ApiSettings('/accounts:lookup' }) // Set response validator. .setResponseValidator((response: any) => { - if (!response.users) { + if (!response.users || !response.users.length) { throw new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); } }); @@ -706,14 +763,14 @@ export const FIREBASE_AUTH_SIGN_UP_NEW_USER = new ApiSettings('/accounts', 'POST if (typeof request.customAttributes !== 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"customAttributes" cannot be set when creating a new user.`, + '"customAttributes" cannot be set when creating a new user.', ); } // signupNewUser does not support validSince. if (typeof request.validSince !== 'undefined') { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"validSince" cannot be set when creating a new user.`, + '"validSince" cannot be set when creating a new user.', ); } // Throw error when tenantId is passed in POST body. @@ -816,7 +873,7 @@ const LIST_OAUTH_IDP_CONFIGS = new ApiSettings('/oauthIdpConfigs', 'GET') request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); } @@ -879,7 +936,7 @@ const LIST_INBOUND_SAML_CONFIGS = new ApiSettings('/inboundSamlConfigs', 'GET') request.pageSize > MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not exceed ` + + 'Required "maxResults" must be a positive integer that does not exceed ' + `${MAX_LIST_PROVIDER_CONFIGURATION_PAGE_SIZE}.`, ); } @@ -955,7 +1012,7 @@ export abstract class AbstractAuthRequestHandler { ); } - this.httpClient = new AuthorizedHttpClient(app); + this.httpClient = new AuthHttpClient(app); } /** @@ -1029,6 +1086,21 @@ export abstract class AbstractAuthRequestHandler { return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); } + public getAccountInfoByFederatedUid(providerId: string, rawId: string): Promise { + if (!validator.isNonEmptyString(providerId) || !validator.isNonEmptyString(rawId)) { + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID); + } + + const request = { + federatedUserId: [{ + providerId, + rawId, + }], + }; + + return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_ACCOUNT_INFO, request); + } + /** * Looks up multiple users by their identifiers (uid, email, etc). * @@ -1039,7 +1111,7 @@ export abstract class AbstractAuthRequestHandler { */ public getAccountInfoByIdentifiers(identifiers: UserIdentifier[]): Promise { if (identifiers.length === 0) { - return Promise.resolve({users: []}); + return Promise.resolve({ users: [] }); } else if (identifiers.length > MAX_GET_ACCOUNTS_BATCH_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, @@ -1241,6 +1313,33 @@ export abstract class AbstractAuthRequestHandler { 'Properties argument must be a non-null object.', ), ); + } else if (validator.isNonNullObject(properties.providerToLink)) { + // TODO(rsgowman): These checks overlap somewhat with + // validateProviderUserInfo. It may be possible to refactor a bit. + if (!validator.isNonEmptyString(properties.providerToLink.providerId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providerToLink.providerId of properties argument must be a non-empty string.'); + } + if (!validator.isNonEmptyString(properties.providerToLink.uid)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providerToLink.uid of properties argument must be a non-empty string.'); + } + } else if (typeof properties.providersToUnlink !== 'undefined') { + if (!validator.isArray(properties.providersToUnlink)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providersToUnlink of properties argument must be an array of strings.'); + } + + properties.providersToUnlink.forEach((providerId) => { + if (!validator.isNonEmptyString(providerId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + 'providersToUnlink of properties argument must be an array of strings.'); + } + }); } // Build the setAccountInfo request. @@ -1275,13 +1374,25 @@ export abstract class AbstractAuthRequestHandler { // It will be removed from the backend request and an additional parameter // deleteProvider: ['phone'] with an array of providerIds (phone in this case), // will be passed. - // Currently this applies to phone provider only. if (request.phoneNumber === null) { - request.deleteProvider = ['phone']; + request.deleteProvider ? request.deleteProvider.push('phone') : request.deleteProvider = ['phone']; delete request.phoneNumber; - } else { - // Doesn't apply to other providers in admin SDK. - delete request.deleteProvider; + } + + if (typeof(request.providerToLink) !== 'undefined') { + request.linkProviderUserInfo = deepCopy(request.providerToLink); + delete request.providerToLink; + + request.linkProviderUserInfo.rawId = request.linkProviderUserInfo.uid; + delete request.linkProviderUserInfo.uid; + } + + if (typeof(request.providersToUnlink) !== 'undefined') { + if (!validator.isArray(request.deleteProvider)) { + request.deleteProvider = []; + } + request.deleteProvider = request.deleteProvider.concat(request.providersToUnlink); + delete request.providersToUnlink; } // Rewrite photoURL to photoUrl. @@ -1369,7 +1480,12 @@ export abstract class AbstractAuthRequestHandler { } // Build the signupNewUser request. - const request: any = deepCopy(properties); + type SignUpNewUserRequest = CreateRequest & { + photoUrl?: string | null; + localId?: string; + mfaInfo?: AuthFactorInfo[]; + }; + const request: SignUpNewUserRequest = deepCopy(properties); // Rewrite photoURL to photoUrl. if (typeof request.photoURL !== 'undefined') { request.photoUrl = request.photoURL; @@ -1385,14 +1501,14 @@ export abstract class AbstractAuthRequestHandler { if (validator.isNonEmptyArray(request.multiFactor.enrolledFactors)) { const mfaInfo: AuthFactorInfo[] = []; try { - request.multiFactor.enrolledFactors.forEach((multiFactorInfo: any) => { + request.multiFactor.enrolledFactors.forEach((multiFactorInfo) => { // Enrollment time and uid are not allowed for signupNewUser endpoint. // They will automatically be provisioned server side. - if (multiFactorInfo.enrollmentTime) { + if ('enrollmentTime' in multiFactorInfo) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"enrollmentTime" is not supported when adding second factors via "createUser()"'); - } else if (multiFactorInfo.uid) { + } else if ('uid' in multiFactorInfo) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '"uid" is not supported when adding second factors via "createUser()"'); @@ -1428,7 +1544,7 @@ export abstract class AbstractAuthRequestHandler { public getEmailActionLink( requestType: string, email: string, actionCodeSettings?: ActionCodeSettings): Promise { - let request = {requestType, email, returnOobLink: true}; + let request = { requestType, email, returnOobLink: true }; // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will // be completed. if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { @@ -1464,7 +1580,7 @@ export abstract class AbstractAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_OAUTH_IDP_CONFIG, {}, { providerId }); } /** @@ -1510,7 +1626,7 @@ export abstract class AbstractAuthRequestHandler { if (!OIDCConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_OAUTH_IDP_CONFIG, {}, { providerId }) .then(() => { // Return nothing. }); @@ -1523,7 +1639,7 @@ export abstract class AbstractAuthRequestHandler { * @return {Promise} A promise that resolves with the newly created OIDC * configuration. */ - public createOAuthIdpConfig(options: AuthProviderConfig): Promise { + public createOAuthIdpConfig(options: OIDCAuthProviderConfig): Promise { // Construct backend request. let request; try { @@ -1532,7 +1648,8 @@ export abstract class AbstractAuthRequestHandler { return Promise.reject(e); } const providerId = options.providerId; - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, {providerId}) + return this.invokeRequestHandler( + this.getProjectConfigUrlBuilder(), CREATE_OAUTH_IDP_CONFIG, request, { providerId }) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1565,7 +1682,7 @@ export abstract class AbstractAuthRequestHandler { } const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_OAUTH_IDP_CONFIG, request, - {providerId, updateMask: updateMask.join(',')}) + { providerId, updateMask: updateMask.join(',') }) .then((response: any) => { if (!OIDCConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1586,7 +1703,7 @@ export abstract class AbstractAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, {providerId}); + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), GET_INBOUND_SAML_CONFIG, {}, { providerId }); } /** @@ -1632,7 +1749,7 @@ export abstract class AbstractAuthRequestHandler { if (!SAMLConfig.isProviderId(providerId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)); } - return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, {providerId}) + return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), DELETE_INBOUND_SAML_CONFIG, {}, { providerId }) .then(() => { // Return nothing. }); @@ -1645,7 +1762,7 @@ export abstract class AbstractAuthRequestHandler { * @return {Promise} A promise that resolves with the newly created SAML * configuration. */ - public createInboundSamlConfig(options: AuthProviderConfig): Promise { + public createInboundSamlConfig(options: SAMLAuthProviderConfig): Promise { // Construct backend request. let request; try { @@ -1655,7 +1772,7 @@ export abstract class AbstractAuthRequestHandler { } const providerId = options.providerId; return this.invokeRequestHandler( - this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, {providerId}) + this.getProjectConfigUrlBuilder(), CREATE_INBOUND_SAML_CONFIG, request, { providerId }) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1688,7 +1805,7 @@ export abstract class AbstractAuthRequestHandler { } const updateMask = utils.generateUpdateMask(request); return this.invokeRequestHandler(this.getProjectConfigUrlBuilder(), UPDATE_INBOUND_SAML_CONFIG, request, - {providerId, updateMask: updateMask.join(',')}) + { providerId, updateMask: updateMask.join(',') }) .then((response: any) => { if (!SAMLConfig.getProviderIdFromResourceName(response.name)) { throw new FirebaseAuthError( @@ -1801,7 +1918,7 @@ const DELETE_TENANT = new ApiSettings('/tenants/{tenantId}', 'DELETE'); /** Instantiates the updateTenant endpoint settings. */ const UPDATE_TENANT = new ApiSettings('/tenants/{tenantId}?updateMask={updateMask}', 'PATCH') -// Set response validator. + // Set response validator. .setResponseValidator((response: any) => { // Response should always contain at least the tenant name. if (!validator.isNonEmptyString(response.name) || @@ -1828,7 +1945,7 @@ const LIST_TENANTS = new ApiSettings('/tenants', 'GET') request.pageSize > MAX_LIST_TENANT_PAGE_SIZE) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive non-zero number that does not exceed ` + + 'Required "maxResults" must be a positive non-zero number that does not exceed ' + `the allowed ${MAX_LIST_TENANT_PAGE_SIZE}.`, ); } @@ -1893,7 +2010,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, {tenantId}) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, GET_TENANT, {}, { tenantId }) .then((response: any) => { return response as TenantServerResponse; }); @@ -1943,7 +2060,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } - return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, {}, {tenantId}) + return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, DELETE_TENANT, {}, { tenantId }) .then(() => { // Return nothing. }); @@ -1955,7 +2072,7 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * @param {TenantOptions} tenantOptions The properties to set on the new tenant to be created. * @return {Promise} A promise that resolves with the newly created tenant object. */ - public createTenant(tenantOptions: TenantOptions): Promise { + public createTenant(tenantOptions: CreateTenantRequest): Promise { try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, true); @@ -1975,16 +2092,18 @@ export class AuthRequestHandler extends AbstractAuthRequestHandler { * @param {TenantOptions} tenantOptions The properties to update on the existing tenant. * @return {Promise} A promise that resolves with the modified tenant object. */ - public updateTenant(tenantId: string, tenantOptions: TenantOptions): Promise { + public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { if (!validator.isNonEmptyString(tenantId)) { return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_TENANT_ID)); } try { // Construct backend request. const request = Tenant.buildServerRequest(tenantOptions, false); - const updateMask = utils.generateUpdateMask(request); + // Do not traverse deep into testPhoneNumbers. The entire content should be replaced + // and not just specific phone numbers. + const updateMask = utils.generateUpdateMask(request, ['testPhoneNumbers']); return this.invokeRequestHandler(this.tenantMgmtResourceBuilder, UPDATE_TENANT, request, - {tenantId, updateMask: updateMask.join(',')}) + { tenantId, updateMask: updateMask.join(',') }) .then((response: any) => { return response as TenantServerResponse; }); @@ -2056,3 +2175,15 @@ export class TenantAwareAuthRequestHandler extends AbstractAuthRequestHandler { return super.uploadAccount(users, options); } } + +function emulatorHost(): string | undefined { + return process.env.FIREBASE_AUTH_EMULATOR_HOST +} + +/** + * When true the SDK should communicate with the Auth Emulator for all API + * calls and also produce unsigned tokens. + */ +export function useEmulator(): boolean { + return !!emulatorHost(); +} diff --git a/src/auth/auth-config.ts b/src/auth/auth-config.ts old mode 100755 new mode 100644 index 95527eb1a7..059d866574 --- a/src/auth/auth-config.ts +++ b/src/auth/auth-config.ts @@ -15,39 +15,20 @@ */ import * as validator from '../utils/validator'; -import {deepCopy} from '../utils/deep-copy'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { deepCopy } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; +import MultiFactorConfigInterface = auth.MultiFactorConfig; +import MultiFactorConfigState = auth.MultiFactorConfigState; +import AuthFactorType = auth.AuthFactorType; +import EmailSignInProviderConfig = auth.EmailSignInProviderConfig; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import OAuthResponseType = auth.OAuthResponseType; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; -/** The filter interface used for listing provider configurations. */ -export interface AuthProviderConfigFilter { - type: 'saml' | 'oidc'; - maxResults?: number; - pageToken?: string; -} - -/** The base Auth provider configuration interface. */ -export interface AuthProviderConfig { - providerId: string; - displayName?: string; - enabled: boolean; -} - -/** The OIDC Auth provider configuration interface. */ -export interface OIDCAuthProviderConfig extends AuthProviderConfig { - clientId: string; - issuer: string; -} - -/** The SAML Auth provider configuration interface. */ -export interface SAMLAuthProviderConfig extends AuthProviderConfig { - idpEntityId: string; - ssoURL: string; - x509Certificates: string[]; - rpEntityId: string; - callbackURL?: string; - enableRequestSigning?: boolean; -} +/** 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 { @@ -95,6 +76,8 @@ export interface OIDCConfigServerRequest { issuer?: string; displayName?: string; enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; [key: string]: any; } @@ -107,57 +90,197 @@ export interface OIDCConfigServerResponse { issuer?: string; displayName?: string; enabled?: boolean; + clientSecret?: string; + responseType?: OAuthResponseType; } -/** The public API response interface for listing provider configs. */ -export interface ListProviderConfigResults { - providerConfigs: AuthProviderConfig[]; - pageToken?: string; +/** The server side email configuration request interface. */ +export interface EmailSignInConfigServerRequest { + allowPasswordSignup?: boolean; + enableEmailLinkSignin?: boolean; } -/** The public API request interface for updating a SAML Auth provider. */ -export interface SAMLUpdateAuthProviderRequest { - idpEntityId?: string; - ssoURL?: string; - x509Certificates?: string[]; - rpEntityId?: string; - callbackURL?: string; - enableRequestSigning?: boolean; - enabled?: boolean; - displayName?: string; +/** 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[]; } -/** The generic request interface for updating/creating a SAML Auth provider. */ -export interface SAMLAuthProviderRequest extends SAMLUpdateAuthProviderRequest { - providerId?: string; -} +/** + * Defines the multi-factor config class used to convert client side MultiFactorConfig + * to a format that is understood by the Auth server. + */ +export class MultiFactorAuthConfig implements MultiFactorConfigInterface { + public readonly state: MultiFactorConfigState; + public readonly factorIds: AuthFactorType[]; -/** The public API request interface for updating an OIDC Auth provider. */ -export interface OIDCUpdateAuthProviderRequest { - clientId?: string; - issuer?: string; - enabled?: boolean; - displayName?: string; -} + /** + * Static method to convert a client side request to a MultiFactorAuthServerConfig. + * Throws an error if validation fails. + * + * @param options The options object to convert to a server request. + * @return The resulting server request. + */ + public static buildServerRequest(options: MultiFactorConfigInterface): MultiFactorAuthServerConfig { + const request: MultiFactorAuthServerConfig = {}; + MultiFactorAuthConfig.validate(options); + if (Object.prototype.hasOwnProperty.call(options, 'state')) { + request.state = options.state; + } + if (Object.prototype.hasOwnProperty.call(options, 'factorIds')) { + (options.factorIds || []).forEach((factorId) => { + if (typeof request.enabledProviders === 'undefined') { + request.enabledProviders = []; + } + request.enabledProviders.push(AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId]); + }); + // In case an empty array is passed. Ensure it gets populated so the array is cleared. + if (options.factorIds && options.factorIds.length === 0) { + request.enabledProviders = []; + } + } + return request; + } -/** The generic request interface for updating/creating an OIDC Auth provider. */ -export interface OIDCAuthProviderRequest extends OIDCUpdateAuthProviderRequest { - providerId?: string; -} + /** + * Validates the MultiFactorConfig options object. Throws an error on failure. + * + * @param options The options object to validate. + */ + private static validate(options: MultiFactorConfigInterface): void { + const validKeys = { + state: true, + factorIds: true, + }; + if (!validator.isNonNullObject(options)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig" must be a non-null object.', + ); + } + // Check for unsupported top level attributes. + for (const key in options) { + if (!(key in validKeys)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid MultiFactorConfig parameter.`, + ); + } + } + // Validate content. + if (typeof options.state !== 'undefined' && + options.state !== 'ENABLED' && + options.state !== 'DISABLED') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".', + ); + } + + if (typeof options.factorIds !== 'undefined') { + if (!validator.isArray(options.factorIds)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".', + ); + } -/** The public API request interface for updating a generic Auth provider. */ -export type UpdateAuthProviderRequest = SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; + // Validate content of array. + options.factorIds.forEach((factorId) => { + if (typeof AUTH_FACTOR_CLIENT_TO_SERVER_TYPE[factorId] === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${factorId}" is not a valid "AuthFactorType".`, + ); + } + }); + } + } -/** The email provider configuration interface. */ -export interface EmailSignInProviderConfig { - enabled?: boolean; - passwordRequired?: boolean; // In the backend API, default is true if not provided + /** + * The MultiFactorAuthConfig constructor. + * + * @param response The server side response used to initialize the + * MultiFactorAuthConfig object. + * @constructor + */ + constructor(response: MultiFactorAuthServerConfig) { + if (typeof response.state === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INTERNAL_ERROR, + 'INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); + } + this.state = response.state; + this.factorIds = []; + (response.enabledProviders || []).forEach((enabledProvider) => { + // Ignore unsupported types. It is possible the current admin SDK version is + // not up to date and newer backend types are supported. + if (typeof AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider] !== 'undefined') { + this.factorIds.push(AUTH_FACTOR_SERVER_TO_CLIENT_TYPE[enabledProvider]); + } + }) + } + + /** @return The plain object representation of the multi-factor config instance. */ + public toJSON(): object { + return { + state: this.state, + factorIds: this.factorIds, + }; + } } -/** The server side email configuration request interface. */ -export interface EmailSignInConfigServerRequest { - allowPasswordSignup?: boolean; - enableEmailLinkSignin?: boolean; + +/** + * 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.` + ); + } + } } @@ -166,7 +289,7 @@ export interface EmailSignInConfigServerRequest { * to a format that is understood by the Auth server. */ export class EmailSignInConfig implements EmailSignInProviderConfig { - public readonly enabled?: boolean; + public readonly enabled: boolean; public readonly passwordRequired?: boolean; /** @@ -284,7 +407,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { * @return {?SAMLConfigServerRequest} The resulting server request or null if not valid. */ public static buildServerRequest( - options: SAMLAuthProviderRequest, + options: Partial, ignoreMissingFields = false): SAMLConfigServerRequest | null { const makeRequest = validator.isNonNullObject(options) && (options.providerId || ignoreMissingFields); @@ -301,12 +424,12 @@ export class SAMLConfig implements SAMLAuthProviderConfig { request.idpConfig = { idpEntityId: options.idpEntityId, ssoUrl: options.ssoURL, - signRequest: options.enableRequestSigning, + signRequest: (options as any).enableRequestSigning, idpCertificates: typeof options.x509Certificates === 'undefined' ? undefined : [], }; if (options.x509Certificates) { for (const cert of (options.x509Certificates || [])) { - request.idpConfig!.idpCertificates!.push({x509Certificate: cert}); + request.idpConfig!.idpCertificates!.push({ x509Certificate: cert }); } } } @@ -349,7 +472,7 @@ export class SAMLConfig implements SAMLAuthProviderConfig { * @param {SAMLAuthProviderRequest} options The options object to validate. * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. */ - public static validate(options: SAMLAuthProviderRequest, ignoreMissingFields = false): void { + public static validate(options: Partial, ignoreMissingFields = false): void { const validKeys = { enabled: true, displayName: true, @@ -435,8 +558,8 @@ export class SAMLConfig implements SAMLAuthProviderConfig { ); } }); - if (typeof options.enableRequestSigning !== 'undefined' && - !validator.isBoolean(options.enableRequestSigning)) { + if (typeof (options as any).enableRequestSigning !== 'undefined' && + !validator.isBoolean((options as any).enableRequestSigning)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, '"SAMLAuthProviderConfig.enableRequestSigning" must be a boolean.', @@ -506,8 +629,8 @@ export class SAMLConfig implements SAMLAuthProviderConfig { this.displayName = response.displayName; } - /** @return {SAMLAuthProviderConfig} The plain object representation of the SAMLConfig. */ - public toJSON(): SAMLAuthProviderConfig { + /** @return The plain object representation of the SAMLConfig. */ + public toJSON(): object { return { enabled: this.enabled, displayName: this.displayName, @@ -532,6 +655,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig { public readonly providerId: string; public readonly issuer: string; public readonly clientId: string; + public readonly clientSecret?: string; + public readonly responseType: OAuthResponseType; /** * Converts a client side request to a OIDCConfigServerRequest which is the format @@ -539,12 +664,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { * Throws an error if validation fails. If the request is not a OIDCConfig request, * returns null. * - * @param {OIDCAuthProviderRequest} options The options object to convert to a server request. - * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. - * @return {?OIDCConfigServerRequest} The resulting server request or null if not valid. + * @param options The options object to convert to a server request. + * @param ignoreMissingFields Whether to ignore missing fields. + * @return The resulting server request or null if not valid. */ public static buildServerRequest( - options: OIDCAuthProviderRequest, + options: Partial, ignoreMissingFields = false): OIDCConfigServerRequest | null { const makeRequest = validator.isNonNullObject(options) && (options.providerId || ignoreMissingFields); @@ -558,6 +683,12 @@ export class OIDCConfig implements OIDCAuthProviderConfig { request.displayName = options.displayName; request.issuer = options.issuer; request.clientId = options.clientId; + if (typeof options.clientSecret !== 'undefined') { + request.clientSecret = options.clientSecret; + } + if (typeof options.responseType !== 'undefined') { + request.responseType = options.responseType; + } return request; } @@ -587,16 +718,22 @@ export class OIDCConfig implements OIDCAuthProviderConfig { /** * Validates the OIDCConfig options object. Throws an error on failure. * - * @param {OIDCAuthProviderRequest} options The options object to validate. - * @param {boolean=} ignoreMissingFields Whether to ignore missing fields. + * @param options The options object to validate. + * @param ignoreMissingFields Whether to ignore missing fields. */ - public static validate(options: OIDCAuthProviderRequest, ignoreMissingFields = false): void { + public static validate(options: Partial, ignoreMissingFields = false): void { const validKeys = { enabled: true, displayName: true, providerId: true, clientId: true, issuer: true, + clientSecret: true, + responseType: true, + }; + const validResponseTypes = { + idToken: true, + code: true, }; if (!validator.isNonNullObject(options)) { throw new FirebaseAuthError( @@ -655,6 +792,59 @@ export class OIDCConfig implements OIDCAuthProviderConfig { '"OIDCAuthProviderConfig.displayName" must be a valid string.', ); } + if (typeof options.clientSecret !== 'undefined' && + !validator.isNonEmptyString(options.clientSecret)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + '"OIDCAuthProviderConfig.clientSecret" must be a valid string.', + ); + } + if (validator.isNonNullObject(options.responseType) && typeof options.responseType !== 'undefined') { + Object.keys(options.responseType).forEach((key) => { + if (!(key in validResponseTypes)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CONFIG, + `"${key}" is not a valid OAuthResponseType parameter.`, + ); + } + }); + + const idToken = options.responseType.idToken; + if (typeof idToken !== 'undefined' && !validator.isBoolean(idToken)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.', + ); + } + + const code = options.responseType.code; + if (typeof code !== 'undefined') { + if (!validator.isBoolean(code)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '"OIDCAuthProviderConfig.responseType.code" must be a boolean.', + ); + } + + // If code flow is enabled, client secret must be provided. + if (code && typeof options.clientSecret === 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.MISSING_OAUTH_CLIENT_SECRET, + 'The OAuth configuration client secret is required to enable OIDC code flow.', + ); + } + } + + const allKeys = Object.keys(options.responseType).length; + const enabledCount = Object.values(options.responseType).filter(Boolean).length; + // Only one of OAuth response types can be set to true. + if (allKeys > 1 && enabledCount != 1) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_OAUTH_RESPONSETYPE, + 'Only exactly one OAuth responseType should be set to true.', + ); + } + } } /** @@ -688,6 +878,13 @@ export class OIDCConfig implements OIDCAuthProviderConfig { // When enabled is undefined, it takes its default value of false. this.enabled = !!response.enabled; this.displayName = response.displayName; + + if (typeof response.clientSecret !== 'undefined') { + this.clientSecret = response.clientSecret; + } + if (typeof response.responseType !== 'undefined') { + this.responseType = response.responseType; + } } /** @return {OIDCAuthProviderConfig} The plain object representation of the OIDCConfig. */ @@ -698,6 +895,8 @@ export class OIDCConfig implements OIDCAuthProviderConfig { providerId: this.providerId, issuer: this.issuer, clientId: this.clientId, + clientSecret: deepCopy(this.clientSecret), + responseType: deepCopy(this.responseType), }; } } diff --git a/src/auth/auth.ts b/src/auth/auth.ts old mode 100755 new mode 100644 index cf83e7b430..48f46c345a --- 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,113 +15,55 @@ * limitations under the License. */ -import {UserRecord, CreateRequest, UpdateRequest} from './user-record'; +import { deepCopy } from '../utils/deep-copy'; +import { UserRecord } from './user-record'; import { - UserIdentifier, isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, + isUidIdentifier, isEmailIdentifier, isPhoneIdentifier, isProviderIdentifier, } from './identifier'; -import {FirebaseApp} from '../firebase-app'; -import {FirebaseTokenGenerator, cryptoSignerFromApp} from './token-generator'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from './token-generator'; import { - AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, + AbstractAuthRequestHandler, AuthRequestHandler, TenantAwareAuthRequestHandler, useEmulator, } from './auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo, FirebaseArrayIndexError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import { - UserImportOptions, UserImportRecord, UserImportResult, -} from './user-import-builder'; - +import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; -import { FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier } from './token-verifier'; -import {ActionCodeSettings} from './action-code-settings-builder'; +import { auth } from './index'; +import { + FirebaseTokenVerifier, createSessionCookieVerifier, createIdTokenVerifier +} from './token-verifier'; import { - AuthProviderConfig, AuthProviderConfigFilter, ListProviderConfigResults, UpdateAuthProviderRequest, SAMLConfig, OIDCConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from './auth-config'; -import {TenantManager} from './tenant-manager'; - - -/** - * 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); - } -} - - -/** Represents the result of the {@link admin.auth.getUsers()} API. */ -export interface GetUsersResult { - /** - * Set of user records, corresponding to the set of users that were - * requested. Only users that were found are listed here. The result set is - * unordered. - */ - users: UserRecord[]; - - /** Set of identifiers that were requested, but not found. */ - notFound: UserIdentifier[]; -} - - -/** Response object for a listUsers operation. */ -export interface ListUsersResult { - users: UserRecord[]; - pageToken?: string; -} - - -/** Response object for deleteUsers operation. */ -export interface DeleteUsersResult { - failureCount: number; - successCount: number; - errors: FirebaseArrayIndexError[]; -} - - -/** Interface representing a decoded ID token. */ -export interface DecodedIdToken { - aud: string; - auth_time: number; - email?: string; - email_verified?: boolean; - exp: number; - firebase: { - identities: { - [key: string]: any; - }; - sign_in_provider: string; - sign_in_second_factor?: string; - second_factor_identifier?: string; - [key: string]: any; - }; - iat: number; - iss: string; - phone_number?: string; - picture?: string; - sub: string; - tenant?: string; - [key: string]: any; -} - - -/** Interface representing the session cookie options. */ -export interface SessionCookieOptions { - expiresIn: number; -} - +import { TenantManager } from './tenant-manager'; +import { cryptoSignerFromApp } from '../utils/crypto-signer'; + +import UserIdentifier = auth.UserIdentifier; +import CreateRequest = auth.CreateRequest; +import UpdateRequest = auth.UpdateRequest; +import ActionCodeSettings = auth.ActionCodeSettings; +import UserImportOptions = auth.UserImportOptions; +import UserImportRecord = auth.UserImportRecord; +import UserImportResult = auth.UserImportResult; +import AuthProviderConfig = auth.AuthProviderConfig; +import AuthProviderConfigFilter = auth.AuthProviderConfigFilter; +import ListProviderConfigResults = auth.ListProviderConfigResults; +import UpdateAuthProviderRequest = auth.UpdateAuthProviderRequest; +import GetUsersResult = auth.GetUsersResult; +import ListUsersResult = auth.ListUsersResult; +import DeleteUsersResult = auth.DeleteUsersResult; +import DecodedIdToken = auth.DecodedIdToken; +import SessionCookieOptions = auth.SessionCookieOptions; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import BaseAuthInterface = auth.BaseAuth; +import AuthInterface = auth.Auth; +import TenantAwareAuthInterface = auth.TenantAwareAuth; /** * Base Auth class. Mainly used for user management APIs. */ -export class BaseAuth { +export class BaseAuth implements BaseAuthInterface { protected readonly tokenGenerator: FirebaseTokenGenerator; protected readonly idTokenVerifier: FirebaseTokenVerifier; @@ -140,8 +83,7 @@ export class BaseAuth { if (tokenGenerator) { this.tokenGenerator = tokenGenerator; } else { - const cryptoSigner = cryptoSignerFromApp(app); - this.tokenGenerator = new FirebaseTokenGenerator(cryptoSigner); + this.tokenGenerator = createFirebaseTokenGenerator(app); } this.sessionCookieVerifier = createSessionCookieVerifier(app); @@ -174,15 +116,16 @@ export class BaseAuth { * verification. */ public verifyIdToken(idToken: string, checkRevoked = false): Promise { - return this.idTokenVerifier.verifyJWT(idToken) + const isEmulator = useEmulator(); + return this.idTokenVerifier.verifyJWT(idToken, isEmulator) .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. - if (!checkRevoked) { - return decodedIdToken; + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.ID_TOKEN_REVOKED); } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.ID_TOKEN_REVOKED); + return decodedIdToken; }); } @@ -231,6 +174,36 @@ export class BaseAuth { }); } + /** + * Gets the user data for the user corresponding to a given provider id. + * + * See [Retrieve user data](/docs/auth/admin/manage-users#retrieve_user_data) + * for code samples and detailed documentation. + * + * @param providerId The provider ID, for example, "google.com" for the + * Google provider. + * @param uid The user identifier for the given provider. + * + * @return A promise fulfilled with the user data corresponding to the + * given provider id. + */ + public getUserByProviderUid(providerId: string, uid: string): Promise { + // Although we don't really advertise it, we want to also handle + // non-federated idps with this call. So if we detect one of them, we'll + // reroute this request appropriately. + if (providerId === 'phone') { + return this.getUserByPhoneNumber(uid); + } else if (providerId === 'email') { + return this.getUserByEmail(uid); + } + + return this.authRequestHandler.getAccountInfoByFederatedUid(providerId, uid) + .then((response: any) => { + // Returns the user record populated with server response. + return new UserRecord(response.users[0]); + }); + } + /** * Gets the user data corresponding to the specified identifiers. * @@ -409,6 +382,50 @@ export class BaseAuth { * @return {Promise} A promise that resolves with the modified user record. */ public updateUser(uid: string, properties: UpdateRequest): Promise { + // Although we don't really advertise it, we want to also handle linking of + // non-federated idps with this call. So if we detect one of them, we'll + // adjust the properties parameter appropriately. This *does* imply that a + // conflict could arise, e.g. if the user provides a phoneNumber property, + // but also provides a providerToLink with a 'phone' provider id. In that + // case, we'll throw an error. + properties = deepCopy(properties); + + if (properties?.providerToLink) { + if (properties.providerToLink.providerId === 'email') { + if (typeof properties.email !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.email and UpdateRequest.providerToLink.providerId='email' were set. To " + + 'link to the email/password provider, only specify the UpdateRequest.email field.'); + } + properties.email = properties.providerToLink.uid; + delete properties.providerToLink; + } else if (properties.providerToLink.providerId === 'phone') { + if (typeof properties.phoneNumber !== 'undefined') { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.phoneNumber and UpdateRequest.providerToLink.providerId='phone' were set. To " + + 'link to a phone provider, only specify the UpdateRequest.phoneNumber field.'); + } + properties.phoneNumber = properties.providerToLink.uid; + delete properties.providerToLink; + } + } + if (properties?.providersToUnlink) { + if (properties.providersToUnlink.indexOf('phone') !== -1) { + // If we've been told to unlink the phone provider both via setting + // phoneNumber to null *and* by setting providersToUnlink to include + // 'phone', then we'll reject that. Though it might also be reasonable + // to relax this restriction and just unlink it. + if (properties.phoneNumber === null) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "Both UpdateRequest.phoneNumber=null and UpdateRequest.providersToUnlink=['phone'] were set. To " + + 'unlink from a phone provider, only specify the UpdateRequest.phoneNumber=null field.'); + } + } + } + return this.authRequestHandler.updateExistingAccount(uid, properties) .then((existingUid) => { // Return the corresponding user record. @@ -424,7 +441,7 @@ export class BaseAuth { * @return {Promise} A promise that resolves when the operation completes * successfully. */ - public setCustomUserClaims(uid: string, customUserClaims: object): Promise { + public setCustomUserClaims(uid: string, customUserClaims: object | null): Promise { return this.authRequestHandler.setCustomUserClaims(uid, customUserClaims) .then(() => { // Return nothing on success. @@ -502,15 +519,16 @@ export class BaseAuth { */ public verifySessionCookie( sessionCookie: string, checkRevoked = false): Promise { - return this.sessionCookieVerifier.verifyJWT(sessionCookie) + const isEmulator = useEmulator(); + return this.sessionCookieVerifier.verifyJWT(sessionCookie, isEmulator) .then((decodedIdToken: DecodedIdToken) => { // Whether to check if the token was revoked. - if (!checkRevoked) { - return decodedIdToken; + if (checkRevoked || isEmulator) { + return this.verifyDecodedJWTNotRevoked( + decodedIdToken, + AuthClientErrorCode.SESSION_COOKIE_REVOKED); } - return this.verifyDecodedJWTNotRevoked( - decodedIdToken, - AuthClientErrorCode.SESSION_COOKIE_REVOKED); + return decodedIdToken; }); } @@ -607,7 +625,7 @@ export class BaseAuth { return Promise.reject( new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"AuthProviderConfigFilter.type" must be either "saml' or "oidc"`)); + '"AuthProviderConfigFilter.type" must be either "saml" or "oidc"')); } /** @@ -691,12 +709,12 @@ export class BaseAuth { )); } if (OIDCConfig.isProviderId(config.providerId)) { - return this.authRequestHandler.createOAuthIdpConfig(config) + return this.authRequestHandler.createOAuthIdpConfig(config as OIDCAuthProviderConfig) .then((response) => { return new OIDCConfig(response); }); } else if (SAMLConfig.isProviderId(config.providerId)) { - return this.authRequestHandler.createInboundSamlConfig(config) + return this.authRequestHandler.createInboundSamlConfig(config as SAMLAuthProviderConfig) .then((response) => { return new SAMLConfig(response); }); @@ -740,7 +758,10 @@ export class BaseAuth { /** * The tenant aware Auth class. */ -export class TenantAwareAuth extends BaseAuth { +export class TenantAwareAuth + extends BaseAuth + implements TenantAwareAuthInterface { + public readonly tenantId: string; /** @@ -751,9 +772,8 @@ export class TenantAwareAuth extends BaseAuth { * @constructor */ constructor(app: FirebaseApp, tenantId: string) { - const cryptoSigner = cryptoSignerFromApp(app); - const tokenGenerator = new FirebaseTokenGenerator(cryptoSigner, tenantId); - super(app, new TenantAwareAuthRequestHandler(app, tenantId), tokenGenerator); + super(app, new TenantAwareAuthRequestHandler(app, tenantId), + createFirebaseTokenGenerator(app, tenantId)); utils.addReadonlyGetter(this, 'tenantId', tenantId); } @@ -837,9 +857,8 @@ export class TenantAwareAuth extends BaseAuth { * Auth service bound to the provided app. * An Auth instance can have multiple tenants. */ -export class Auth extends BaseAuth implements FirebaseServiceInterface { +export class Auth extends BaseAuth implements AuthInterface { - public INTERNAL: AuthInternals = new AuthInternals(); private readonly tenantManager_: TenantManager; private readonly app_: FirebaseApp; @@ -867,3 +886,13 @@ export class Auth extends BaseAuth implements FirebaseServic return this.tenantManager_; } } + +function createFirebaseTokenGenerator(app: FirebaseApp, + tenantId?: string): FirebaseTokenGenerator { + try { + const signer = useEmulator() ? new EmulatedSigner() : cryptoSignerFromApp(app); + return new FirebaseTokenGenerator(signer, tenantId); + } catch (err) { + throw handleCryptoSignerError(err); + } +} diff --git a/src/auth/identifier.ts b/src/auth/identifier.ts index 46ade5d5cb..b9e93b1fc0 100644 --- a/src/auth/identifier.ts +++ b/src/auth/identifier.ts @@ -14,49 +14,16 @@ * limitations under the License. */ -/** - * Used for looking up an account by uid. - * - * See auth.getUsers() - */ -export interface UidIdentifier { - uid: string; -} - -/** - * Used for looking up an account by email. - * - * See auth.getUsers() - */ -export interface EmailIdentifier { - email: string; -} - -/** - * Used for looking up an account by phone number. - * - * See auth.getUsers() - */ -export interface PhoneIdentifier { - phoneNumber: string; -} - -/** - * Used for looking up an account by federated provider. - * - * See auth.getUsers() - */ -export interface ProviderIdentifier { - providerId: string; - providerUid: string; -} +import { auth } from './index'; -/** - * Identifies a user to be looked up. - */ -export type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; +import UserIdentifier = auth.UserIdentifier; +import UidIdentifier = auth.UidIdentifier; +import EmailIdentifier = auth.EmailIdentifier; +import PhoneIdentifier = auth.PhoneIdentifier; +import ProviderIdentifier = auth.ProviderIdentifier; -/* User defined type guards. See +/* + * User defined type guards. See * https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards */ diff --git a/src/auth.d.ts b/src/auth/index.ts similarity index 79% rename from src/auth.d.ts rename to src/auth/index.ts index f17a84b675..6193336beb 100644 --- a/src/auth.d.ts +++ b/src/auth/index.ts @@ -1,13 +1,50 @@ -import * as _admin from './index.d'; - -/* eslint-disable @typescript-eslint/ban-types */ - -export namespace admin.auth { - +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app, FirebaseArrayIndexError } from '../firebase-namespace-api'; + +/** + * Gets the {@link auth.Auth `Auth`} service for the default app or a + * given app. + * + * `admin.auth()` can be called with no arguments to access the default app's + * {@link auth.Auth `Auth`} service or as `admin.auth(app)` to access the + * {@link auth.Auth `Auth`} service associated with a specific app. + * + * @example + * ```javascript + * // Get the Auth service for the default app + * var defaultAuth = admin.auth(); + * ``` + * + * @example + * ```javascript + * // Get the Auth service for a given app + * var otherAuth = admin.auth(otherApp); + * ``` + * + */ +export declare function auth(app?: app.App): auth.Auth; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace auth { /** - * Interface representing a user's metadata. - */ - interface UserMetadata { + * Interface representing a user's metadata. + */ + export interface UserMetadata { /** * The date the user last signed in, formatted as a UTC string. @@ -24,19 +61,19 @@ export namespace admin.auth { * formatted as a UTC Date string (eg 'Sat, 03 Feb 2001 04:05:06 GMT'). * Returns null if the user was never active. */ - lastRefreshTime?: string|null; + lastRefreshTime?: string | null; /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * Interface representing a user's info from a third-party identity provider * such as Google or Facebook. */ - interface UserInfo { + export interface UserInfo { /** * The user identifier for the linked provider. @@ -71,13 +108,13 @@ export namespace admin.auth { /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** - * Interface representing the common properties of a user enrolled second factor. + * Interface representing the common properties of a user-enrolled second factor. */ - interface MultiFactorInfo { + export interface MultiFactorInfo { /** * The ID of the enrolled second factor. This ID is unique to the user. @@ -102,13 +139,13 @@ export namespace admin.auth { /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** - * Interface representing a phone specific user enrolled second factor. + * Interface representing a phone specific user-enrolled second factor. */ - interface PhoneMultiFactorInfo extends MultiFactorInfo { + export interface PhoneMultiFactorInfo extends MultiFactorInfo { /** * The phone number associated with a phone second factor. @@ -116,10 +153,46 @@ export namespace admin.auth { phoneNumber: 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 a user. */ - interface UserRecord { + export interface UserRecord { /** * The user's `uid`. @@ -160,12 +233,12 @@ export namespace admin.auth { /** * Additional metadata about the user. */ - metadata: admin.auth.UserMetadata; + metadata: UserMetadata; /** * An array of providers (for example, Google, Facebook) linked to the user. */ - providerData: admin.auth.UserInfo[]; + providerData: UserInfo[]; /** * The user's hashed password (base64-encoded), only if Firebase Auth hashing @@ -173,7 +246,7 @@ export namespace admin.auth { * when uploading this user, as is typical when migrating from another Auth * system, this will be an empty string. If no password is set, this is * null. This is only available when the user is obtained from - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`}. + * {@link auth.Auth.listUsers `listUsers()`}. * */ passwordHash?: string; @@ -184,7 +257,7 @@ export namespace admin.auth { * upload this user, typical when migrating from another Auth system, this will * be an empty string. If no password is set, this is null. This is only * available when the user is obtained from - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`}. + * {@link auth.Auth.listUsers `listUsers()`}. * */ passwordSalt?: string; @@ -193,14 +266,14 @@ export namespace admin.auth { * The user's custom claims object if available, typically used to define * user roles and propagated to an authenticated user's ID token. * This is set via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#setCustomUserClaims `setCustomUserClaims()`} + * {@link auth.Auth.setCustomUserClaims `setCustomUserClaims()`} */ - customClaims?: {[key: string]: any}; + customClaims?: { [key: string]: any }; /** * The date the user's tokens are valid after, formatted as a UTC string. * This is updated every time the user's refresh token are revoked either - * from the {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#revokeRefreshTokens `revokeRefreshTokens()`} + * from the {@link auth.Auth.revokeRefreshTokens `revokeRefreshTokens()`} * API or from the Firebase Auth backend on big account changes (password * resets, password or email updates, etc). */ @@ -214,59 +287,59 @@ export namespace admin.auth { /** * The multi-factor related properties for the current user, if available. */ - multiFactor?: admin.auth.MultiFactorSettings; + multiFactor?: MultiFactorSettings; /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; } /** * The multi-factor related user settings. */ - interface MultiFactorSettings { + export interface MultiFactorSettings { /** * List of second factors enrolled with the current user. * Currently only phone second factors are supported. */ - enrolledFactors: admin.auth.MultiFactorInfo[]; + enrolledFactors: MultiFactorInfo[]; /** * @return A JSON-serializable representation of this multi-factor object. */ - toJSON(): Object; + toJSON(): object; } /** * The multi-factor related user settings for create operations. */ - interface MultiFactorCreateSettings { + export interface MultiFactorCreateSettings { /** * The created user's list of enrolled second factors. */ - enrolledFactors: admin.auth.CreateMultiFactorInfoRequest[]; + enrolledFactors: CreateMultiFactorInfoRequest[]; } /** * The multi-factor related user settings for update operations. */ - interface MultiFactorUpdateSettings { + export interface MultiFactorUpdateSettings { /** * The updated list of enrolled second factors. The provided list overwrites the user's * existing list of second factors. * When null is passed, all of the user's existing second factors are removed. */ - enrolledFactors: admin.auth.UpdateMultiFactorInfoRequest[] | null; + enrolledFactors: UpdateMultiFactorInfoRequest[] | null; } /** - * Interface representing common properties of a user enrolled second factor + * Interface representing common properties of a user-enrolled second factor * for an `UpdateRequest`. */ - interface UpdateMultiFactorInfoRequest { + export interface BaseUpdateMultiFactorInfoRequest { /** * The ID of the enrolled second factor. This ID is unique to the user. When not provided, @@ -291,10 +364,10 @@ export namespace admin.auth { } /** - * Interface representing a phone specific user enrolled second factor + * Interface representing a phone specific user-enrolled second factor * for an `UpdateRequest`. */ - interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { + export interface UpdatePhoneMultiFactorInfoRequest extends BaseUpdateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -302,10 +375,16 @@ export namespace admin.auth { phoneNumber: string; } + /** + * Type representing the properties of a user-enrolled second factor + * for an `UpdateRequest`. + */ + export type UpdateMultiFactorInfoRequest = | UpdatePhoneMultiFactorInfoRequest; + /** * Interface representing the properties to update on the provided user. */ - interface UpdateRequest { + export interface UpdateRequest { /** * Whether or not the user is disabled: `true` for disabled; @@ -346,14 +425,34 @@ export namespace admin.auth { /** * The user's updated multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorUpdateSettings; + 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[]; } /** - * Interface representing base properties of a user enrolled second factor for a + * Interface representing base properties of a user-enrolled second factor for a * `CreateRequest`. */ - interface CreateMultiFactorInfoRequest { + export interface BaseCreateMultiFactorInfoRequest { /** * The optional display name for an enrolled second factor. @@ -367,10 +466,10 @@ export namespace admin.auth { } /** - * Interface representing a phone specific user enrolled second factor for a + * Interface representing a phone specific user-enrolled second factor for a * `CreateRequest`. */ - interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { + export interface CreatePhoneMultiFactorInfoRequest extends BaseCreateMultiFactorInfoRequest { /** * The phone number associated with a phone second factor. @@ -378,11 +477,17 @@ export namespace admin.auth { phoneNumber: string; } + /** + * Type representing the properties of a user-enrolled second factor + * for a `CreateRequest`. + */ + export type CreateMultiFactorInfoRequest = | CreatePhoneMultiFactorInfoRequest; + /** * Interface representing the properties to set on a new user record to be * created. */ - interface CreateRequest extends UpdateRequest { + export interface CreateRequest extends UpdateRequest { /** * The user's `uid`. @@ -392,19 +497,19 @@ export namespace admin.auth { /** * The user's multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorCreateSettings; + multiFactor?: MultiFactorCreateSettings; } /** * Interface representing a decoded Firebase ID token, returned from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#verifyIdToken `verifyIdToken()`} method. + * {@link auth.Auth.verifyIdToken `verifyIdToken()`} method. * * Firebase ID tokens are OpenID Connect spec-compliant JSON Web Tokens (JWTs). * See the * [ID Token section of the OpenID Connect spec](http://openid.net/specs/openid-connect-core-1_0.html#IDToken) * for more information about the specific properties below. */ - interface DecodedIdToken { + export interface DecodedIdToken { /** * The audience for which this token is intended. @@ -466,7 +571,13 @@ export namespace admin.auth { /** * The ID of the provider used to sign in the user. * One of `"anonymous"`, `"password"`, `"facebook.com"`, `"github.com"`, - * `"google.com"`, `"twitter.com"`, or `"custom"`. + * `"google.com"`, `"twitter.com"`, `"apple.com"`, `"microsoft.com"`, + * "yahoo.com"`, `"phone"`, `"playgames.google.com"`, `"gc.apple.com"`, + * or `"custom"`. + * + * Additional Identity Platform provider IDs include `"linkedin.com"`, + * OIDC and SAML identity providers prefixed with `"saml."` and `"oidc."` + * respectively. */ sign_in_provider: string; @@ -538,8 +649,8 @@ export namespace admin.auth { [key: string]: any; } - /** Represents the result of the {@link admin.auth.getUsers()} API. */ - interface GetUsersResult { + /** Represents the result of the {@link auth.Auth.getUsers} API. */ + export interface GetUsersResult { /** * Set of user records, corresponding to the set of users that were * requested. Only users that were found are listed here. The result set is @@ -553,16 +664,16 @@ export namespace admin.auth { /** * Interface representing the object returned from a - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listUsers `listUsers()`} operation. Contains the list + * {@link auth.Auth.listUsers `listUsers()`} operation. Contains the list * of users for the current batch and the next page token if available. */ - interface ListUsersResult { + export interface ListUsersResult { /** - * The list of {@link admin.auth.UserRecord `UserRecord`} objects for the + * The list of {@link auth.UserRecord `UserRecord`} objects for the * current downloaded batch. */ - users: admin.auth.UserRecord[]; + users: UserRecord[]; /** * The next page token if available. This is needed for the next batch download. @@ -570,16 +681,16 @@ export namespace admin.auth { pageToken?: string; } - type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | + export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; /** * Interface representing the user import options needed for - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method. This is used to + * {@link auth.Auth.importUsers `importUsers()`} method. This is used to * provide the password hashing algorithm information. */ - interface UserImportOptions { + export interface UserImportOptions { /** * The password hashing information. @@ -642,10 +753,10 @@ export namespace admin.auth { /** * Interface representing the response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method for batch + * {@link auth.Auth.importUsers `importUsers()`} method for batch * importing users to Firebase Auth. */ - interface UserImportResult { + export interface UserImportResult { /** * The number of user records that failed to import to Firebase Auth. @@ -661,15 +772,15 @@ export namespace admin.auth { * An array of errors corresponding to the provided users to import. The * length of this array is equal to [`failureCount`](#failureCount). */ - errors: _admin.FirebaseArrayIndexError[]; + errors: FirebaseArrayIndexError[]; } /** * Represents the result of the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#deleteUsers `deleteUsers()`} + * {@link auth.Auth.deleteUsers `deleteUsers()`} * API. */ - interface DeleteUsersResult { + export interface DeleteUsersResult { /** * The number of user records that failed to be deleted (possibly zero). */ @@ -687,13 +798,13 @@ export namespace admin.auth { * were encountered during the deletion. Length of this list is equal to * the return value of [`failureCount`](#failureCount). */ - errors: _admin.FirebaseArrayIndexError[]; + errors: FirebaseArrayIndexError[]; } /** * User metadata to include when importing a user. */ - interface UserMetadataRequest { + export interface UserMetadataRequest { /** * The date the user last signed in, formatted as a UTC string. @@ -709,7 +820,7 @@ export namespace admin.auth { /** * User provider data to include when importing a user. */ - interface UserProviderRequest { + export interface UserProviderRequest { /** * The user identifier for the linked provider. @@ -744,9 +855,9 @@ export namespace admin.auth { /** * Interface representing a user to import to Firebase Auth via the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#importUsers `importUsers()`} method. + * {@link auth.Auth.importUsers `importUsers()`} method. */ - interface UserImportRecord { + export interface UserImportRecord { /** * The user's `uid`. @@ -787,23 +898,23 @@ export namespace admin.auth { /** * Additional metadata about the user. */ - metadata?: admin.auth.UserMetadataRequest; + metadata?: UserMetadataRequest; /** * An array of providers (for example, Google, Facebook) linked to the user. */ - providerData?: admin.auth.UserProviderRequest[]; + providerData?: UserProviderRequest[]; /** * The user's custom claims object if available, typically used to define * user roles and propagated to an authenticated user's ID token. */ - customClaims?: {[key: string]: any}; + customClaims?: { [key: string]: any }; /** * The buffer of bytes representing the user's hashed password. * When a user is to be imported with a password hash, - * {@link admin.auth.UserImportOptions `UserImportOptions`} are required to be + * {@link auth.UserImportOptions `UserImportOptions`} are required to be * specified to identify the hashing algorithm used to generate this hash. */ passwordHash?: Buffer; @@ -820,19 +931,19 @@ export namespace admin.auth { * When not provided in an `admin.auth.TenantAwareAuth` context, the user is uploaded * to the tenant corresponding to that `TenantAwareAuth` instance's tenant ID. */ - tenantId?: string | null; + tenantId?: string; /** * The user's multi-factor related properties. */ - multiFactor?: admin.auth.MultiFactorUpdateSettings; + multiFactor?: MultiFactorUpdateSettings; } /** * Interface representing the session cookie options needed for the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createSessionCookie `createSessionCookie()`} method. + * {@link auth.Auth.createSessionCookie `createSessionCookie()`} method. */ - interface SessionCookieOptions { + export interface SessionCookieOptions { /** * The session cookie custom expiration in milliseconds. The minimum allowed is @@ -845,7 +956,7 @@ export namespace admin.auth { * This is the interface that defines the required continue/state URL with * optional Android and iOS bundle identifiers. */ - interface ActionCodeSettings { + export interface ActionCodeSettings { /** * Defines the link continue/state URL, which has different meanings in @@ -942,7 +1053,7 @@ export namespace admin.auth { * All other settings of a tenant will also be inherited. These will need to be managed * from the Cloud Console UI. */ - interface Tenant { + export interface Tenant { /** * The tenant identifier. @@ -971,16 +1082,75 @@ export namespace admin.auth { passwordRequired?: boolean; }; + /** + * Whether the anonymous provider is enabled. + */ + anonymousSignInEnabled: boolean; + + /** + * The multi-factor auth configuration on the current tenant. + */ + multiFactorConfig?: MultiFactorConfig; + + /** + * The map containing the test phone number / code pairs for the tenant. + */ + testPhoneNumbers?: { [phoneNumber: string]: string }; + /** * @return A JSON-serializable representation of this object. */ - toJSON(): Object; + toJSON(): object; + } + + /** + * 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[]; + } + + /** + * The email sign in configuration. + */ + export interface EmailSignInProviderConfig { + /** + * Whether email provider is enabled. + */ + enabled: boolean; + + /** + * Whether password is required for email sign-in. When not required, + * email sign-in can be performed with password or via email link sign-in. + */ + passwordRequired?: boolean; // In the backend API, default is true if not provided } /** * Interface representing the properties to update on the provided tenant. */ - interface UpdateTenantRequest { + export interface UpdateTenantRequest { /** * The tenant display name. @@ -990,38 +1160,42 @@ export namespace admin.auth { /** * The email sign in configuration. */ - emailSignInConfig?: { + emailSignInConfig?: EmailSignInProviderConfig; - /** - * Whether email provider is enabled. - */ - enabled: boolean; + /** + * Whether the anonymous provider is enabled. + */ + anonymousSignInEnabled?: 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; - }; + /** + * 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; } /** * Interface representing the properties to set on a new tenant. */ - type CreateTenantRequest = UpdateTenantRequest; + export type CreateTenantRequest = UpdateTenantRequest; /** * Interface representing the object returned from a - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listTenants `listTenants()`} + * {@link auth.TenantManager.listTenants `listTenants()`} * operation. * Contains the list of tenants for the current batch and the next page token if available. */ - interface ListTenantsResult { + export interface ListTenantsResult { /** - * The list of {@link admin.auth.Tenant `Tenant`} objects for the downloaded batch. + * The list of {@link auth.Tenant `Tenant`} objects for the downloaded batch. */ - tenants: admin.auth.Tenant[]; + tenants: Tenant[]; /** * The next page token if available. This is needed for the next batch download. @@ -1032,9 +1206,9 @@ export namespace admin.auth { /** * The filter interface used for listing provider configurations. This is used * when specifying how to list configured identity providers via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listProviderConfigs `listProviderConfigs()`}. + * {@link auth.Auth.listProviderConfigs `listProviderConfigs()`}. */ - interface AuthProviderConfigFilter { + export interface AuthProviderConfigFilter { /** * The Auth provider configuration filter. This can be either `saml` or `oidc`. @@ -1059,7 +1233,7 @@ export namespace admin.auth { /** * The base Auth provider configuration interface. */ - interface AuthProviderConfig { + export interface BaseAuthProviderConfig { /** * The provider ID defined by the developer. @@ -1072,7 +1246,7 @@ export namespace admin.auth { * The user-friendly display name to the current configuration. This name is * also used as the provider label in the Cloud Console. */ - displayName: string; + displayName?: string; /** * Whether the provider configuration is enabled or disabled. A user @@ -1085,9 +1259,9 @@ export namespace admin.auth { * The * [SAML](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) * Auth provider configuration interface. A SAML provider can be created via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createProviderConfig `createProviderConfig()`}. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - interface SAMLAuthProviderConfig extends admin.auth.AuthProviderConfig { + export interface SAMLAuthProviderConfig extends BaseAuthProviderConfig { /** * The SAML IdP entity identifier. @@ -1127,12 +1301,33 @@ export namespace admin.auth { 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 https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#createProviderConfig `createProviderConfig()`}. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. */ - interface OIDCAuthProviderConfig extends admin.auth.AuthProviderConfig { + export interface OIDCAuthProviderConfig extends BaseAuthProviderConfig { /** * This is the required client ID used to confirm the audience of an OIDC @@ -1159,14 +1354,30 @@ export namespace admin.auth { * [spec](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation). */ issuer: string; + + /** + * The OIDC provider's client secret to enable OIDC code flow. + */ + clientSecret?: string; + + /** + * The OIDC provider's response object for OAuth authorization flow. + */ + responseType?: OAuthResponseType; } + /** + * The Auth provider configuration type. + * {@link auth.Auth.createProviderConfig `createProviderConfig()`}. + */ + export type AuthProviderConfig = SAMLAuthProviderConfig | OIDCAuthProviderConfig; + /** * The request interface for updating a SAML Auth provider. This is used * when updating a SAML provider's configuration via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateProviderConfig `updateProviderConfig()`}. + * {@link auth.Auth.updateProviderConfig `updateProviderConfig()`}. */ - interface SAMLUpdateAuthProviderRequest { + export interface SAMLUpdateAuthProviderRequest { /** * The SAML provider's updated display name. If not provided, the existing @@ -1214,9 +1425,9 @@ export namespace admin.auth { /** * The request interface for updating an OIDC Auth provider. This is used * when updating an OIDC provider's configuration via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#updateProviderConfig `updateProviderConfig()`}. + * {@link auth.Auth.updateProviderConfig `updateProviderConfig()`}. */ - interface OIDCUpdateAuthProviderRequest { + export interface OIDCUpdateAuthProviderRequest { /** * The OIDC provider's updated display name. If not provided, the existing @@ -1241,19 +1452,30 @@ export namespace admin.auth { * configuration's value is not modified. */ issuer?: string; + + /** + * The OIDC provider's client secret to enable OIDC code flow. + * If not provided, the existing configuration's value is not modified. + */ + clientSecret?: string; + + /** + * The OIDC provider's response object for OAuth authorization flow. + */ + responseType?: OAuthResponseType; } /** * The response interface for listing provider configs. This is only available * when listing all identity providers' configurations via - * {@link https://firebase.google.com/docs/reference/admin/node/admin.auth.Auth#listProviderConfigs `listProviderConfigs()`}. + * {@link auth.Auth.listProviderConfigs `listProviderConfigs()`}. */ - interface ListProviderConfigResults { + export interface ListProviderConfigResults { /** * The list of providers for the specified type in the current page. */ - providerConfigs: admin.auth.AuthProviderConfig[]; + providerConfigs: AuthProviderConfig[]; /** * The next page token, if available. @@ -1261,15 +1483,15 @@ export namespace admin.auth { pageToken?: string; } - type UpdateAuthProviderRequest = - admin.auth.SAMLUpdateAuthProviderRequest | admin.auth.OIDCUpdateAuthProviderRequest; + export type UpdateAuthProviderRequest = + SAMLUpdateAuthProviderRequest | OIDCUpdateAuthProviderRequest; /** * Used for looking up an account by uid. * * See auth.getUsers() */ - interface UidIdentifier { + export interface UidIdentifier { uid: string; } @@ -1278,7 +1500,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface EmailIdentifier { + export interface EmailIdentifier { email: string; } @@ -1287,7 +1509,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface PhoneIdentifier { + export interface PhoneIdentifier { phoneNumber: string; } @@ -1296,7 +1518,7 @@ export namespace admin.auth { * * See auth.getUsers() */ - interface ProviderIdentifier { + export interface ProviderIdentifier { providerId: string; providerUid: string; } @@ -1304,9 +1526,10 @@ export namespace admin.auth { /** * Identifies a user to be looked up. */ - type UserIdentifier = UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; + export type UserIdentifier = + UidIdentifier | EmailIdentifier | PhoneIdentifier | ProviderIdentifier; - interface BaseAuth { + export interface BaseAuth { /** * Creates a new Firebase custom token (JWT) that can be sent back to a client @@ -1324,7 +1547,7 @@ export namespace admin.auth { * @return A promise fulfilled with a custom token for the * provided `uid` and payload. */ - createCustomToken(uid: string, developerClaims?: Object): Promise; + createCustomToken(uid: string, developerClaims?: object): Promise; /** * Creates a new user. @@ -1338,7 +1561,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the newly created user. */ - createUser(properties: admin.auth.CreateRequest): Promise; + createUser(properties: CreateRequest): Promise; /** * Deletes an existing user. @@ -1375,7 +1598,7 @@ export namespace admin.auth { * deletions, as well as the array of errors that corresponds to the * failed deletions. */ - deleteUsers(uids: string[]): Promise; + deleteUsers(uids: string[]): Promise; /** * Gets the user data for the user corresponding to a given `uid`. @@ -1388,7 +1611,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided `uid`. */ - getUser(uid: string): Promise; + getUser(uid: string): Promise; /** * Gets the user data for the user corresponding to a given email. @@ -1402,7 +1625,7 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided email. */ - getUserByEmail(email: string): Promise; + getUserByEmail(email: string): Promise; /** * Gets the user data for the user corresponding to a given phone number. The @@ -1417,7 +1640,22 @@ export namespace admin.auth { * @return A promise fulfilled with the user * data corresponding to the provided phone number. */ - getUserByPhoneNumber(phoneNumber: string): Promise; + getUserByPhoneNumber(phoneNumber: string): Promise; + + /** + * Gets the user data for the user corresponding to a given provider ID. + * + * See [Retrieve user data](/docs/auth/admin/manage-users#retrieve_user_data) + * for code samples and detailed documentation. + * + * @param providerId The provider ID, for example, "google.com" for the + * Google provider. + * @param uid The user identifier for the given provider. + * + * @return A promise fulfilled with the user data corresponding to the + * given provider id. + */ + getUserByProviderUid(providerId: string, uid: string): Promise; /** * Gets the user data corresponding to the specified identifiers. @@ -1434,7 +1672,7 @@ export namespace admin.auth { * @throws FirebaseAuthError If any of the identifiers are invalid or if more than 100 * identifiers are specified. */ - getUsers(identifiers: admin.auth.UserIdentifier[]): Promise; + getUsers(identifiers: UserIdentifier[]): Promise; /** * Retrieves a list of users (single batch only) with a size of `maxResults` @@ -1451,7 +1689,7 @@ export namespace admin.auth { * @return A promise that resolves with * the current batch of downloaded users and the next page token. */ - listUsers(maxResults?: number, pageToken?: string): Promise; + listUsers(maxResults?: number, pageToken?: string): Promise; /** * Updates an existing user. @@ -1459,14 +1697,14 @@ export namespace admin.auth { * See [Update a user](/docs/auth/admin/manage-users#update_a_user) for code * samples and detailed documentation. * - * @param uid The `uid` corresponding to the user to delete. + * @param uid The `uid` corresponding to the user to update. * @param properties The properties to update on * the provided user. * * @return A promise fulfilled with the * updated user data. */ - updateUser(uid: string, properties: admin.auth.UpdateRequest): Promise; + updateUser(uid: string, properties: UpdateRequest): Promise; /** * Verifies a Firebase ID token (JWT). If the token is valid, the promise is @@ -1488,7 +1726,7 @@ export namespace admin.auth { * token's decoded claims if the ID token is valid; otherwise, a rejected * promise. */ - verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; + verifyIdToken(idToken: string, checkRevoked?: boolean): Promise; /** * Sets additional developer claims on an existing user identified by the @@ -1513,13 +1751,13 @@ export namespace admin.auth { * @return A promise that resolves when the operation completes * successfully. */ - setCustomUserClaims(uid: string, customUserClaims: Object | null): Promise; + setCustomUserClaims(uid: string, customUserClaims: object | null): Promise; /** * Revokes all refresh tokens for an existing user. * * This API will update the user's - * {@link admin.auth.UserRecord#tokensValidAfterTime `tokensValidAfterTime`} to + * {@link auth.UserRecord.tokensValidAfterTime `tokensValidAfterTime`} to * the current UTC. It is important that the server on which this is called has * its clock set correctly and synchronized. * @@ -1527,7 +1765,7 @@ export namespace admin.auth { * new ID tokens for existing sessions from getting minted, existing ID tokens * may remain active until their natural expiration (one hour). To verify that * ID tokens are revoked, use - * {@link admin.auth.Auth#verifyIdToken `verifyIdToken(idToken, true)`} + * {@link auth.Auth.verifyIdToken `verifyIdToken(idToken, true)`} * where `checkRevoked` is set to true. * * @param uid The `uid` corresponding to the user whose refresh tokens @@ -1542,7 +1780,7 @@ export namespace admin.auth { * Imports the provided list of users into Firebase Auth. * A maximum of 1000 users are allowed to be imported one at a time. * When importing users with passwords, - * {@link admin.auth.UserImportOptions `UserImportOptions`} are required to be + * {@link auth.UserImportOptions `UserImportOptions`} are required to be * specified. * This operation is optimized for bulk imports and will ignore checks on `uid`, * `email` and other identifier uniqueness which could result in duplications. @@ -1556,9 +1794,9 @@ export namespace admin.auth { * corresponding errors. */ importUsers( - users: admin.auth.UserImportRecord[], - options?: admin.auth.UserImportOptions, - ): Promise; + users: UserImportRecord[], + options?: UserImportOptions, + ): Promise; /** * Creates a new Firebase session cookie with the specified options. The created @@ -1579,7 +1817,7 @@ export namespace admin.auth { */ createSessionCookie( idToken: string, - sessionCookieOptions: admin.auth.SessionCookieOptions, + sessionCookieOptions: SessionCookieOptions, ): Promise; /** @@ -1606,12 +1844,12 @@ export namespace admin.auth { verifySessionCookie( sessionCookie: string, checkForRevocation?: boolean, - ): Promise; + ): Promise; /** * Generates the out of band email action link to reset a user's password. * The link is generated for the user with the specified email address. The - * optional {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object + * optional {@link auth.ActionCodeSettings `ActionCodeSettings`} object * defines whether the link is to be handled by a mobile app or browser and the * additional state information to be passed in the deep link, etc. * @@ -1659,13 +1897,13 @@ export namespace admin.auth { */ generatePasswordResetLink( email: string, - actionCodeSettings?: admin.auth.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise; /** * Generates the out of band email action link to verify the user's ownership * of the specified email. The - * {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object provided + * {@link auth.ActionCodeSettings `ActionCodeSettings`} object provided * as an argument to this method defines whether the link is to be handled by a * mobile app or browser along with additional state information to be passed in * the deep link, etc. @@ -1713,13 +1951,13 @@ export namespace admin.auth { */ generateEmailVerificationLink( email: string, - actionCodeSettings?: admin.auth.ActionCodeSettings, + actionCodeSettings?: ActionCodeSettings, ): Promise; /** * Generates the out of band email action link to sign in or sign up the owner * of the specified email. The - * {@link admin.auth.ActionCodeSettings `ActionCodeSettings`} object provided + * {@link auth.ActionCodeSettings `ActionCodeSettings`} object provided * as an argument to this method defines whether the link is to be handled by a * mobile app or browser along with additional state information to be passed in * the deep link, etc. @@ -1767,7 +2005,7 @@ export namespace admin.auth { */ generateSignInWithEmailLink( email: string, - actionCodeSettings: admin.auth.ActionCodeSettings, + actionCodeSettings: ActionCodeSettings, ): Promise; /** @@ -1783,8 +2021,8 @@ export namespace admin.auth { * filter requirements. */ listProviderConfigs( - options: admin.auth.AuthProviderConfigFilter - ): Promise; + options: AuthProviderConfigFilter + ): Promise; /** * Looks up an Auth provider configuration by the provided ID. @@ -1801,7 +2039,7 @@ export namespace admin.auth { * @return A promise that resolves * with the configuration corresponding to the provided ID. */ - getProviderConfig(providerId: string): Promise; + getProviderConfig(providerId: string): Promise; /** * Deletes the provider configuration corresponding to the provider ID passed. @@ -1834,8 +2072,8 @@ export namespace admin.auth { * @return A promise that resolves with the updated provider configuration. */ updateProviderConfig( - providerId: string, updatedConfig: admin.auth.UpdateAuthProviderRequest - ): Promise; + providerId: string, updatedConfig: UpdateAuthProviderRequest + ): Promise; /** * Returns a promise that resolves with the newly created `AuthProviderConfig` @@ -1849,8 +2087,8 @@ export namespace admin.auth { * @return A promise that resolves with the created provider configuration. */ createProviderConfig( - config: admin.auth.AuthProviderConfig - ): Promise; + config: AuthProviderConfig + ): Promise; } /** @@ -1870,7 +2108,7 @@ export namespace admin.auth { * `TenantAwareAuth` instances for a specific `tenantId` can be instantiated by calling * `auth.tenantManager().authForTenant(tenantId)`. */ - interface TenantAwareAuth extends BaseAuth { + export interface TenantAwareAuth extends BaseAuth { /** * The tenant identifier corresponding to this `TenantAwareAuth` instance. @@ -1880,13 +2118,13 @@ export namespace admin.auth { tenantId: string; } - interface Auth extends admin.auth.BaseAuth { - app: _admin.app.App; + export interface Auth extends BaseAuth { + app: app.App; /** * @return The tenant manager instance associated with the current project. */ - tenantManager(): admin.auth.TenantManager; + tenantManager(): TenantManager; } /** @@ -1900,13 +2138,13 @@ export namespace admin.auth { * email link generation, etc) in the context of a specified tenant. * */ - interface TenantManager { + export interface TenantManager { /** * @param tenantId The tenant ID whose `TenantAwareAuth` instance is to be returned. * * @return The `TenantAwareAuth` instance corresponding to this tenant identifier. */ - authForTenant(tenantId: string): admin.auth.TenantAwareAuth; + authForTenant(tenantId: string): TenantAwareAuth; /** * Gets the tenant configuration for the tenant corresponding to a given `tenantId`. @@ -1915,7 +2153,7 @@ export namespace admin.auth { * * @return A promise fulfilled with the tenant configuration to the provided `tenantId`. */ - getTenant(tenantId: string): Promise; + getTenant(tenantId: string): Promise; /** * Retrieves a list of tenants (single batch only) with a size of `maxResults` @@ -1930,7 +2168,7 @@ export namespace admin.auth { * @return A promise that resolves with * a batch of downloaded tenants and the next page token. */ - listTenants(maxResults?: number, pageToken?: string): Promise; + listTenants(maxResults?: number, pageToken?: string): Promise; /** * Deletes an existing tenant. @@ -1951,7 +2189,7 @@ export namespace admin.auth { * @return A promise fulfilled with the tenant configuration corresponding to the newly * created tenant. */ - createTenant(tenantOptions: admin.auth.CreateTenantRequest): Promise; + createTenant(tenantOptions: CreateTenantRequest): Promise; /** * Updates an existing tenant configuration. @@ -1961,6 +2199,6 @@ export namespace admin.auth { * * @return A promise fulfilled with the update tenant data. */ - updateTenant(tenantId: string, tenantOptions: admin.auth.UpdateTenantRequest): Promise; + updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise; } } diff --git a/src/auth/tenant-manager.ts b/src/auth/tenant-manager.ts index 73950a76bd..f97f0aaefc 100644 --- a/src/auth/tenant-manager.ts +++ b/src/auth/tenant-manager.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import {AuthRequestHandler} from './auth-api-request'; -import {FirebaseApp} from '../firebase-app'; -import {TenantAwareAuth} from './auth'; -import { - Tenant, TenantServerResponse, ListTenantsResult, TenantOptions, -} from './tenant'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthRequestHandler } from './auth-api-request'; +import { FirebaseApp } from '../firebase-app'; +import { TenantAwareAuth } from './auth'; +import { Tenant, TenantServerResponse } from './tenant'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import * as validator from '../utils/validator'; +import { auth } from './index'; + +import ListTenantsResult = auth.ListTenantsResult; +import TenantManagerInterface = auth.TenantManager; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** * Data structure used to help manage tenant related operations. @@ -30,7 +34,7 @@ import * as validator from '../utils/validator'; * - Getting a TenantAwareAuth instance for running Auth related operations (user mgmt, provider config mgmt, etc) * in the context of a specified tenant. */ -export class TenantManager { +export class TenantManager implements TenantManagerInterface { private readonly authRequestHandler: AuthRequestHandler; private readonly tenantsMap: {[key: string]: TenantAwareAuth}; @@ -126,7 +130,7 @@ export class TenantManager { * @param tenantOptions The properties to set on the new tenant to be created. * @return A promise that resolves with the newly created tenant. */ - public createTenant(tenantOptions: TenantOptions): Promise { + public createTenant(tenantOptions: CreateTenantRequest): Promise { return this.authRequestHandler.createTenant(tenantOptions) .then((response: TenantServerResponse) => { return new Tenant(response); @@ -140,7 +144,7 @@ export class TenantManager { * @param tenantOptions The properties to update on the existing tenant. * @return A promise that resolves with the modified tenant. */ - public updateTenant(tenantId: string, tenantOptions: TenantOptions): Promise { + public updateTenant(tenantId: string, tenantOptions: UpdateTenantRequest): Promise { return this.authRequestHandler.updateTenant(tenantId, tenantOptions) .then((response: TenantServerResponse) => { return new Tenant(response); diff --git a/src/auth/tenant.ts b/src/auth/tenant.ts old mode 100755 new mode 100644 index 0b2ed1dca9..392494739b --- a/src/auth/tenant.ts +++ b/src/auth/tenant.ts @@ -15,20 +15,23 @@ */ import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { deepCopy } from '../utils/deep-copy'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; import { - EmailSignInConfig, EmailSignInConfigServerRequest, EmailSignInProviderConfig, + EmailSignInConfig, EmailSignInConfigServerRequest, MultiFactorAuthServerConfig, + MultiFactorAuthConfig, validateTestPhoneNumbers, } from './auth-config'; +import { auth } from './index'; -/** The TenantOptions interface used for create/read/update tenant operations. */ -export interface TenantOptions { - displayName?: string; - emailSignInConfig?: EmailSignInProviderConfig; -} +import TenantInterface = auth.Tenant; +import UpdateTenantRequest = auth.UpdateTenantRequest; /** The corresponding server side representation of a TenantOptions object. */ export interface TenantOptionsServerRequest extends EmailSignInConfigServerRequest { displayName?: string; + enableAnonymousUser?: boolean; + mfaConfig?: MultiFactorAuthServerConfig; + testPhoneNumbers?: {[key: string]: string}; } /** The tenant server response interface. */ @@ -37,22 +40,21 @@ export interface TenantServerResponse { displayName?: string; allowPasswordSignup?: boolean; enableEmailLinkSignin?: boolean; + enableAnonymousUser?: boolean; + mfaConfig?: MultiFactorAuthServerConfig; + testPhoneNumbers?: {[key: string]: string}; } -/** The interface representing the listTenant API response. */ -export interface ListTenantsResult { - tenants: Tenant[]; - pageToken?: string; -} - - /** * Tenant class that defines a Firebase Auth tenant. */ -export class Tenant { +export class Tenant implements TenantInterface { public readonly tenantId: string; public readonly displayName?: string; public readonly emailSignInConfig?: EmailSignInConfig; + public readonly anonymousSignInEnabled: boolean; + public readonly multiFactorConfig?: MultiFactorAuthConfig; + public readonly testPhoneNumbers?: {[phoneNumber: string]: string}; /** * Builds the corresponding server request for a TenantOptions object. @@ -62,7 +64,7 @@ export class Tenant { * @return {object} The equivalent server request. */ public static buildServerRequest( - tenantOptions: TenantOptions, createRequest: boolean): TenantOptionsServerRequest { + tenantOptions: UpdateTenantRequest, createRequest: boolean): TenantOptionsServerRequest { Tenant.validate(tenantOptions, createRequest); let request: TenantOptionsServerRequest = {}; if (typeof tenantOptions.emailSignInConfig !== 'undefined') { @@ -71,6 +73,16 @@ export class Tenant { 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 ?? {}; + } return request; } @@ -99,6 +111,9 @@ export class Tenant { const validKeys = { displayName: true, emailSignInConfig: true, + anonymousSignInEnabled: true, + multiFactorConfig: true, + testPhoneNumbers: true, }; const label = createRequest ? 'CreateTenantRequest' : 'UpdateTenantRequest'; if (!validator.isNonNullObject(request)) { @@ -129,15 +144,31 @@ export class Tenant { // This will throw an error if invalid. EmailSignInConfig.buildServerRequest(request.emailSignInConfig); } + // Validate test phone numbers if provided. + if (typeof request.testPhoneNumbers !== 'undefined' && + request.testPhoneNumbers !== null) { + validateTestPhoneNumbers(request.testPhoneNumbers); + } else if (request.testPhoneNumbers === null && createRequest) { + // null allowed only for update operations. + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + `"${label}.testPhoneNumbers" must be a non-null object.`, + ); + } + // Validate multiFactorConfig type if provided. + if (typeof request.multiFactorConfig !== 'undefined') { + // This will throw an error if invalid. + MultiFactorAuthConfig.buildServerRequest(request.multiFactorConfig); + } } /** * The Tenant object constructor. * - * @param {any} response The server side response used to initialize the Tenant object. + * @param response The server side response used to initialize the Tenant object. * @constructor */ - constructor(response: any) { + constructor(response: TenantServerResponse) { const tenantId = Tenant.getTenantIdFromResourceName(response.name); if (!tenantId) { throw new FirebaseAuthError( @@ -155,15 +186,32 @@ export class Tenant { 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 || {}); + } } /** @return {object} The plain object representation of the tenant. */ public toJSON(): object { - return { + const json = { tenantId: this.tenantId, displayName: this.displayName, - emailSignInConfig: this.emailSignInConfig && this.emailSignInConfig.toJSON(), + emailSignInConfig: this.emailSignInConfig?.toJSON(), + anonymousSignInEnabled: this.anonymousSignInEnabled, + multiFactorConfig: this.multiFactorConfig?.toJSON(), + testPhoneNumbers: this.testPhoneNumbers, }; + if (typeof json.multiFactorConfig === 'undefined') { + delete json.multiFactorConfig; + } + if (typeof json.testPhoneNumbers === 'undefined') { + delete json.testPhoneNumbers; + } + return json; } } diff --git a/src/auth/token-generator.ts b/src/auth/token-generator.ts index 1cdebd49bd..6c464ec5f2 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,16 +15,18 @@ * limitations under the License. */ -import { FirebaseApp } from '../firebase-app'; -import {ServiceAccountCredential} from './credential'; -import {AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; -import { AuthorizedHttpClient, HttpError, HttpRequestConfig, HttpClient } from '../utils/api-request'; +import { + AuthClientErrorCode, ErrorInfo, FirebaseAuthError +} from '../utils/error'; +import { CryptoSigner, CryptoSignerError, CryptoSignerErrorCode } from '../utils/crypto-signer'; import * as validator from '../utils/validator'; import { toWebSafeBase64 } from '../utils'; +import { Algorithm } from 'jsonwebtoken'; +import { HttpError } from '../utils/api-request'; +const ALGORITHM_NONE: Algorithm = 'none' as const; -const ALGORITHM_RS256 = 'RS256'; const ONE_HOUR_IN_SECONDS = 60 * 60; // List of blacklisted claims which cannot be provided when creating a custom token @@ -35,26 +38,6 @@ export const BLACKLISTED_CLAIMS = [ // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -/** - * CryptoSigner interface represents an object that can be used to sign JWTs. - */ -export interface CryptoSigner { - /** - * Cryptographically signs a buffer of data. - * - * @param {Buffer} buffer The data to be signed. - * @return {Promise} A promise that resolves with the raw bytes of a signature. - */ - sign(buffer: Buffer): Promise; - - /** - * Returns the ID of the service account used to sign tokens. - * - * @return {Promise} A promise that resolves with a service account ID. - */ - getAccountId(): Promise; -} - /** * Represents the header of a JWT. */ @@ -78,159 +61,29 @@ interface JWTBody { } /** - * 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. + * A CryptoSigner implementation that is used when communicating with the Auth emulator. + * It produces unsigned tokens. */ -export class ServiceAccountSigner implements CryptoSigner { - - /** - * Creates a new CryptoSigner instance from the given service account credential. - * - * @param {ServiceAccountCredential} credential A service account credential. - */ - constructor(private readonly credential: ServiceAccountCredential) { - if (!credential) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - 'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.', - ); - } - } +export class EmulatedSigner implements CryptoSigner { - /** - * @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 { - private readonly httpClient: AuthorizedHttpClient; - private serviceAccountId?: string; - - constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) { - if (!httpClient) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'INTERNAL ASSERT: Must provide a HTTP client to initialize IAMSigner.', - ); - } - if (typeof serviceAccountId !== 'undefined' && !validator.isNonEmptyString(serviceAccountId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - 'INTERNAL ASSERT: Service account ID must be undefined or a non-empty string.', - ); - } - this.httpClient = httpClient; - this.serviceAccountId = serviceAccountId; - } + algorithm = ALGORITHM_NONE; /** * @inheritDoc */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars public sign(buffer: Buffer): Promise { - return this.getAccountId().then((serviceAccount) => { - const request: HttpRequestConfig = { - method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`, - data: {payload: buffer.toString('base64')}, - }; - return this.httpClient.send(request); - }).then((response: any) => { - // Response from IAM is base64 encoded. Decode it into a buffer and return. - return Buffer.from(response.data.signedBlob, 'base64'); - }).catch((err) => { - if (err instanceof HttpError) { - const error = err.response.data; - if (validator.isNonNullObject(error) && error.error) { - const errorCode = error.error.status; - const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + - 'for more details on how to use and troubleshoot this feature.'; - const errorMsg = `${error.error.message}; ${description}`; - - throw FirebaseAuthError.fromServerError(errorCode, errorMsg, error); - } - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'Error returned from server: ' + error + '. Additionally, an ' + - 'internal error occurred while attempting to extract the ' + - 'errorcode from the error.', - ); - } - throw err; - }); + return Promise.resolve(Buffer.from('')); } /** * @inheritDoc */ public getAccountId(): Promise { - if (validator.isNonEmptyString(this.serviceAccountId)) { - return Promise.resolve(this.serviceAccountId); - } - const request: HttpRequestConfig = { - method: 'GET', - url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', - headers: { - 'Metadata-Flavor': 'Google', - }, - }; - const client = new HttpClient(); - return client.send(request).then((response) => { - if (!response.text) { - throw new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - 'HTTP Response missing payload', - ); - } - this.serviceAccountId = response.text; - return response.text; - }).catch((err) => { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - `Failed to determine service account. Make sure to initialize ` + - `the SDK with a service account credential. Alternatively specify a service ` + - `account with iam.serviceAccounts.signBlob permission. Original error: ${err}`, - ); - }); + return Promise.resolve('firebase-auth-emulator@example.com'); } } -/** - * Create a new CryptoSigner instance for the given app. If the app has been initialized with a service - * account credential, creates a ServiceAccountSigner. Otherwise creates an IAMSigner. - * - * @param {FirebaseApp} app A FirebaseApp instance. - * @return {CryptoSigner} A CryptoSigner instance. - */ -export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner { - const credential = app.options.credential; - if (credential instanceof ServiceAccountCredential) { - return new ServiceAccountSigner(credential); - } - - return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId); -} - /** * Class for generating different types of Firebase Auth tokens (JWTs). */ @@ -250,7 +103,7 @@ export class FirebaseTokenGenerator { 'INTERNAL ASSERT: Must provide a CryptoSigner to use FirebaseTokenGenerator.', ); } - if (typeof tenantId !== 'undefined' && !validator.isNonEmptyString(tenantId)) { + if (typeof this.tenantId !== 'undefined' && !validator.isNonEmptyString(this.tenantId)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, '`tenantId` argument must be a non-empty string.'); @@ -298,7 +151,7 @@ export class FirebaseTokenGenerator { } return this.signer.getAccountId().then((account) => { const header: JWTHeader = { - alg: ALGORITHM_RS256, + alg: this.signer.algorithm, typ: 'JWT', }; const iat = Math.floor(Date.now() / 1000); @@ -319,9 +172,12 @@ export class FirebaseTokenGenerator { } const token = `${this.encodeSegment(header)}.${this.encodeSegment(body)}`; const signPromise = this.signer.sign(Buffer.from(token)); + return Promise.all([token, signPromise]); }).then(([token, signature]) => { return `${token}.${this.encodeSegment(signature)}`; + }).catch((err) => { + throw handleCryptoSignerError(err); }); } @@ -344,3 +200,44 @@ export class FirebaseTokenGenerator { } } +/** + * Creates a new FirebaseAuthError by extracting the error code, message and other relevant + * details from a CryptoSignerError. + * + * @param {Error} err The Error to convert into a FirebaseAuthError error + * @return {FirebaseAuthError} A Firebase Auth error that can be returned to the user. + */ +export function handleCryptoSignerError(err: Error): Error { + if (!(err instanceof CryptoSignerError)) { + return err; + } + if (err.code === CryptoSignerErrorCode.SERVER_ERROR && validator.isNonNullObject(err.cause)) { + const httpError = err.cause; + const errorResponse = (httpError as HttpError).response.data; + if (validator.isNonNullObject(errorResponse) && errorResponse.error) { + const errorCode = errorResponse.error.status; + const description = 'Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens ' + + 'for more details on how to use and troubleshoot this feature.'; + const errorMsg = `${errorResponse.error.message}; ${description}`; + + return FirebaseAuthError.fromServerError(errorCode, errorMsg, errorResponse); + } + return new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, + 'Error returned from server: ' + errorResponse + '. Additionally, an ' + + 'internal error occurred while attempting to extract the ' + + 'errorcode from the error.' + ); + } + return new FirebaseAuthError(mapToAuthClientErrorCode(err.code), err.message); +} + +function mapToAuthClientErrorCode(code: string): ErrorInfo { + switch (code) { + case CryptoSignerErrorCode.INVALID_CREDENTIAL: + return AuthClientErrorCode.INVALID_CREDENTIAL; + case CryptoSignerErrorCode.INVALID_ARGUMENT: + return AuthClientErrorCode.INVALID_ARGUMENT; + default: + return AuthClientErrorCode.INTERNAL_ERROR; + } +} diff --git a/src/auth/token-verifier.ts b/src/auth/token-verifier.ts index cc0c4f4c02..e100cc25f6 100644 --- a/src/auth/token-verifier.ts +++ b/src/auth/token-verifier.ts @@ -14,20 +14,21 @@ * limitations under the License. */ -import {AuthClientErrorCode, FirebaseAuthError, ErrorInfo} from '../utils/error'; - +import { AuthClientErrorCode, FirebaseAuthError, ErrorInfo } from '../utils/error'; import * as util from '../utils/index'; import * as validator from '../utils/validator'; -import * as jwt from 'jsonwebtoken'; -import { HttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; -import { DecodedIdToken } from './auth'; +import { + DecodedToken, decodeJwt, JwtError, JwtErrorCode, + EmulatorSignatureVerifier, PublicKeySignatureVerifier, ALGORITHM_RS256, SignatureVerifier, +} from '../utils/jwt'; import { FirebaseApp } from '../firebase-app'; +import { auth } from './index'; + +import DecodedIdToken = auth.DecodedIdToken; // Audience to use for Firebase Auth Custom tokens const FIREBASE_AUDIENCE = 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit'; -export const ALGORITHM_RS256 = 'RS256'; - // URL containing the public keys for the Google certs (whose private keys are used to sign Firebase // Auth ID tokens) const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; @@ -35,6 +36,8 @@ const CLIENT_CERT_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/secur // URL containing the public keys for Firebase session cookies. This will be updated to a different URL soon. const SESSION_COOKIE_CERT_URL = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys'; +const EMULATOR_VERIFIER = new EmulatorSignatureVerifier(); + /** User facing token information related to the Firebase ID token. */ export const ID_TOKEN_INFO: FirebaseTokenInfo = { url: 'https://firebase.google.com/docs/auth/admin/verify-id-tokens', @@ -68,75 +71,72 @@ export interface FirebaseTokenInfo { } /** - * Class for verifying general purpose Firebase JWTs. This verifies ID tokens and session cookies. + * Class for verifying ID tokens and session cookies. */ export class FirebaseTokenVerifier { - private publicKeys: {[key: string]: string}; - private publicKeysExpireAt: number; private readonly shortNameArticle: string; + private readonly signatureVerifier: SignatureVerifier; - constructor(private clientCertUrl: string, private algorithm: string, - private issuer: string, private tokenInfo: FirebaseTokenInfo, + constructor(clientCertUrl: string, private issuer: string, private tokenInfo: FirebaseTokenInfo, private readonly app: FirebaseApp) { + if (!validator.isURL(clientCertUrl)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided public client certificate URL is an invalid URL.`, - ); - } else if (!validator.isNonEmptyString(algorithm)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT algorithm is an empty string.`, + '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.`, + 'The provided JWT issuer is an invalid URL.', ); } else if (!validator.isNonNullObject(tokenInfo)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT information is not an object or null.`, + 'The provided JWT information is not an object or null.', ); } else if (!validator.isURL(tokenInfo.url)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The provided JWT verification documentation URL is invalid.`, + 'The provided JWT verification documentation URL is invalid.', ); } else if (!validator.isNonEmptyString(tokenInfo.verifyApiName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT verify API name must be a non-empty string.`, + 'The JWT verify API name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.jwtName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT public full name must be a non-empty string.`, + 'The JWT public full name must be a non-empty string.', ); } else if (!validator.isNonEmptyString(tokenInfo.shortName)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT public short name must be a non-empty string.`, + 'The JWT public short name must be a non-empty string.', ); } else if (!validator.isNonNullObject(tokenInfo.expiredErrorCode) || !('code' in tokenInfo.expiredErrorCode)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `The JWT expiration error code must be a non-null ErrorInfo object.`, + 'The JWT expiration error code must be a non-null ErrorInfo object.', ); } this.shortNameArticle = tokenInfo.shortName.charAt(0).match(/[aeiou]/i) ? 'an' : 'a'; + this.signatureVerifier = + PublicKeySignatureVerifier.withCertificateUrl(clientCertUrl, app.options.httpAgent); + // For backward compatibility, the project ID is validated in the verification call. } /** * Verifies the format and signature of a Firebase Auth JWT token. * - * @param {string} jwtToken The Firebase Auth JWT token to verify. - * @return {Promise} A promise fulfilled with the decoded claims of the Firebase Auth ID - * token. + * @param jwtToken The Firebase Auth JWT token to verify. + * @param isEmulator Whether to accept Auth Emulator tokens. + * @return A promise fulfilled with the decoded claims of the Firebase Auth ID token. */ - public verifyJWT(jwtToken: string): Promise { + public verifyJWT(jwtToken: string, isEmulator = false): Promise { if (!validator.isString(jwtToken)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -144,38 +144,78 @@ export class FirebaseTokenVerifier { ); } - return util.findProjectId(this.app) + return this.ensureProjectId() .then((projectId) => { - return this.verifyJWTWithProjectId(jwtToken, projectId); + return this.decodeAndVerify(jwtToken, projectId, isEmulator); + }) + .then((decoded) => { + const decodedIdToken = decoded.payload as DecodedIdToken; + decodedIdToken.uid = decodedIdToken.sub; + return decodedIdToken; }); } - private verifyJWTWithProjectId(jwtToken: string, projectId: string | null): Promise { - if (!validator.isNonEmptyString(projectId)) { - throw new FirebaseAuthError( - AuthClientErrorCode.INVALID_CREDENTIAL, - `Must initialize app with a cert credential or set your Firebase project ID as the ` + - `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, - ); - } + private ensureProjectId(): Promise { + return util.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_CREDENTIAL, + 'Must initialize app with a cert credential or set your Firebase project ID as the ' + + `GOOGLE_CLOUD_PROJECT environment variable to call ${this.tokenInfo.verifyApiName}.`, + ); + } + return Promise.resolve(projectId); + }) + } - const fullDecodedToken: any = jwt.decode(jwtToken, { - complete: true, - }); + private decodeAndVerify(token: string, projectId: string, isEmulator: boolean): Promise { + return this.safeDecode(token) + .then((decodedToken) => { + this.verifyContent(decodedToken, projectId, isEmulator); + return this.verifySignature(token, isEmulator) + .then(() => decodedToken); + }); + } + + private safeDecode(jwtToken: string): Promise { + return decodeJwt(jwtToken) + .catch((err: JwtError) => { + if (err.code == JwtErrorCode.INVALID_ARGUMENT) { + const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; + const errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed ` + + `the entire string JWT which represents ${this.shortNameArticle} ` + + `${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, + errorMessage); + } + throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, err.message); + }); + } + /** + * Verifies the content of a Firebase Auth JWT. + * + * @param fullDecodedToken The decoded JWT. + * @param projectId The Firebase Project Id. + * @param isEmulator Whether the token is an Emulator token. + */ + private verifyContent( + fullDecodedToken: DecodedToken, + projectId: string | null, + isEmulator: boolean): void { + const header = fullDecodedToken && fullDecodedToken.header; const payload = fullDecodedToken && fullDecodedToken.payload; const projectIdMatchMessage = ` Make sure the ${this.tokenInfo.shortName} comes from the same ` + - `Firebase project as the service account used to authenticate this SDK.`; + 'Firebase project as the service account used to authenticate this SDK.'; const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; let errorMessage: string | undefined; - if (!fullDecodedToken) { - errorMessage = `Decoding ${this.tokenInfo.jwtName} failed. Make sure you passed the entire string JWT ` + - `which represents ${this.shortNameArticle} ${this.tokenInfo.shortName}.` + verifyJwtTokenDocsMessage; - } else if (typeof header.kid === 'undefined') { + if (!isEmulator && typeof header.kid === 'undefined') { const isCustomToken = (payload.aud === FIREBASE_AUDIENCE); const isLegacyCustomToken = (header.alg === 'HS256' && payload.v === 0 && 'd' in payload && 'uid' in payload.d); @@ -190,17 +230,17 @@ export class FirebaseTokenVerifier { } errorMessage += verifyJwtTokenDocsMessage; - } else if (header.alg !== this.algorithm) { - errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + this.algorithm + `" but got ` + - `"` + header.alg + `".` + verifyJwtTokenDocsMessage; + } else if (!isEmulator && header.alg !== ALGORITHM_RS256) { + errorMessage = `${this.tokenInfo.jwtName} has incorrect algorithm. Expected "` + ALGORITHM_RS256 + '" but got ' + + '"' + header.alg + '".' + verifyJwtTokenDocsMessage; } else if (payload.aud !== projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "aud" (audience) claim. Expected "` + - projectId + `" but got "` + payload.aud + `".` + projectIdMatchMessage + + projectId + '" but got "' + payload.aud + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (payload.iss !== this.issuer + projectId) { errorMessage = `${this.tokenInfo.jwtName} has incorrect "iss" (issuer) claim. Expected ` + - `"${this.issuer}"` + projectId + `" but got "` + - payload.iss + `".` + projectIdMatchMessage + verifyJwtTokenDocsMessage; + `"${this.issuer}` + projectId + '" but got "' + + payload.iss + '".' + projectIdMatchMessage + verifyJwtTokenDocsMessage; } else if (typeof payload.sub !== 'string') { errorMessage = `${this.tokenInfo.jwtName} has no "sub" (subject) claim.` + verifyJwtTokenDocsMessage; } else if (payload.sub === '') { @@ -210,155 +250,72 @@ export class FirebaseTokenVerifier { verifyJwtTokenDocsMessage; } if (errorMessage) { - return Promise.reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); + throw new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage); } - - return this.fetchPublicKeys().then((publicKeys) => { - if (!Object.prototype.hasOwnProperty.call(publicKeys, header.kid)) { - return Promise.reject( - new FirebaseAuthError( - AuthClientErrorCode.INVALID_ARGUMENT, - `${this.tokenInfo.jwtName} has "kid" claim which does not correspond to a known public key. ` + - `Most likely the ${this.tokenInfo.shortName} is expired, so get a fresh token from your ` + - `client app and try again.`, - ), - ); - } else { - return this.verifyJwtSignatureWithKey(jwtToken, publicKeys[header.kid]); - } - - }); } - /** - * Verifies the JWT signature using the provided public key. - * @param {string} jwtToken The JWT token to verify. - * @param {string} publicKey The public key certificate. - * @return {Promise} A promise that resolves with the decoded JWT claims on successful - * verification. - */ - private verifyJwtSignatureWithKey(jwtToken: string, publicKey: string): Promise { - const verifyJwtTokenDocsMessage = ` See ${this.tokenInfo.url} ` + - `for details on how to retrieve ${this.shortNameArticle} ${this.tokenInfo.shortName}.`; - return new Promise((resolve, reject) => { - jwt.verify(jwtToken, publicKey, { - algorithms: [this.algorithm], - }, (error: jwt.VerifyErrors, decodedToken: string | object) => { - if (error) { - if (error.name === 'TokenExpiredError') { - const errorMessage = `${this.tokenInfo.jwtName} has expired. Get a fresh ${this.tokenInfo.shortName}` + - ` from your client app and try again (auth/${this.tokenInfo.expiredErrorCode.code}).` + - verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(this.tokenInfo.expiredErrorCode, errorMessage)); - } else if (error.name === 'JsonWebTokenError') { - const errorMessage = `${this.tokenInfo.jwtName} has invalid signature.` + verifyJwtTokenDocsMessage; - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, errorMessage)); - } - return reject(new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message)); - } else { - // TODO(rsgowman): I think the typing on jwt.verify is wrong. It claims that this can be either a string or an - // object, but the code always seems to call it as an object. Investigate and upstream typing changes if this - // is actually correct. - if (typeof decodedToken === 'string') { - return reject(new FirebaseAuthError( - AuthClientErrorCode.INTERNAL_ERROR, - "Unexpected decodedToken. Expected an object but got a string: '" + decodedToken + "'", - )); - } else { - const decodedIdToken = (decodedToken as DecodedIdToken); - decodedIdToken.uid = decodedIdToken.sub; - resolve(decodedIdToken); - } - } + private verifySignature(jwtToken: string, isEmulator: boolean): + Promise { + const verifier = isEmulator ? EMULATOR_VERIFIER : this.signatureVerifier; + return verifier.verify(jwtToken) + .catch((error) => { + throw this.mapJwtErrorToAuthError(error); }); - }); } /** - * Fetches the public keys for the Google certs. - * - * @return {Promise} A promise fulfilled with public keys for the Google certs. + * Maps JwtError to FirebaseAuthError + * + * @param error JwtError to be mapped. + * @returns FirebaseAuthError or Error instance. */ - private fetchPublicKeys(): Promise<{[key: string]: string}> { - const publicKeysExist = (typeof this.publicKeys !== 'undefined'); - const publicKeysExpiredExists = (typeof this.publicKeysExpireAt !== 'undefined'); - const publicKeysStillValid = (publicKeysExpiredExists && Date.now() < this.publicKeysExpireAt); - if (publicKeysExist && publicKeysStillValid) { - return Promise.resolve(this.publicKeys); + 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); } - - const client = new HttpClient(); - const request: HttpRequestConfig = { - method: 'GET', - url: this.clientCertUrl, - httpAgent: this.app.options.httpAgent, - }; - return client.send(request).then((resp) => { - if (!resp.isJson() || resp.data.error) { - // Treat all non-json messages and messages with an 'error' field as - // error responses. - throw new HttpError(resp); - } - if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) { - const cacheControlHeader: string = resp.headers['cache-control']; - const parts = cacheControlHeader.split(','); - parts.forEach((part) => { - const subParts = part.trim().split('='); - if (subParts[0] === 'max-age') { - const maxAge: number = +subParts[1]; - this.publicKeysExpireAt = Date.now() + (maxAge * 1000); - } - }); - } - this.publicKeys = resp.data; - return resp.data; - }).catch((err) => { - if (err instanceof HttpError) { - let errorMessage = 'Error fetching public keys for Google certs: '; - const resp = err.response; - if (resp.isJson() && resp.data.error) { - errorMessage += `${resp.data.error}`; - if (resp.data.error_description) { - errorMessage += ' (' + resp.data.error_description + ')'; - } - } else { - errorMessage += `${resp.text}`; - } - throw new FirebaseAuthError(AuthClientErrorCode.INTERNAL_ERROR, errorMessage); - } - throw err; - }); + return new FirebaseAuthError(AuthClientErrorCode.INVALID_ARGUMENT, error.message); } } /** * Creates a new FirebaseTokenVerifier to verify Firebase ID tokens. * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} + * @param app Firebase app instance. + * @return FirebaseTokenVerifier */ export function createIdTokenVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( CLIENT_CERT_URL, - ALGORITHM_RS256, 'https://securetoken.google.com/', ID_TOKEN_INFO, - app, + app ); } /** * Creates a new FirebaseTokenVerifier to verify Firebase session cookies. * - * @param {FirebaseApp} app Firebase app instance. - * @return {FirebaseTokenVerifier} + * @param app Firebase app instance. + * @return FirebaseTokenVerifier */ export function createSessionCookieVerifier(app: FirebaseApp): FirebaseTokenVerifier { return new FirebaseTokenVerifier( SESSION_COOKIE_CERT_URL, - ALGORITHM_RS256, 'https://session.firebase.google.com/', SESSION_COOKIE_INFO, - app, + app ); } diff --git a/src/auth/user-import-builder.ts b/src/auth/user-import-builder.ts old mode 100755 new mode 100644 index 852f7740ce..92e84cc08c --- a/src/auth/user-import-builder.ts +++ b/src/auth/user-import-builder.ts @@ -14,72 +14,18 @@ * limitations under the License. */ -import {deepCopy, deepExtend} from '../utils/deep-copy'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; import * as utils from '../utils'; import * as validator from '../utils/validator'; -import {AuthClientErrorCode, FirebaseAuthError, FirebaseArrayIndexError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { FirebaseArrayIndexError } from '../firebase-namespace-api'; +import { auth } from './index'; -/** Firebase Auth supported hashing algorithms for import operations. */ -export type HashAlgorithmType = 'SCRYPT' | 'STANDARD_SCRYPT' | 'HMAC_SHA512' | - 'HMAC_SHA256' | 'HMAC_SHA1' | 'HMAC_MD5' | 'MD5' | 'PBKDF_SHA1' | 'BCRYPT' | - 'PBKDF2_SHA256' | 'SHA512' | 'SHA256' | 'SHA1'; - - -/** User import options for bulk account imports. */ -export interface UserImportOptions { - hash: { - algorithm: HashAlgorithmType; - key?: Buffer; - saltSeparator?: Buffer; - rounds?: number; - memoryCost?: number; - parallelization?: number; - blockSize?: number; - derivedKeyLength?: number; - }; -} - -interface SecondFactor { - uid: string; - phoneNumber: string; - displayName?: string; - enrollmentTime?: string; - factorId: string; -} - -interface UserProviderRequest { - uid: string; - displayName?: string; - email?: string; - phoneNumber?: string; - photoURL?: string; - providerId: string; -} - -interface UserMetadataRequest { - lastSignInTime?: string; - creationTime?: string; -} - -/** User import record as accepted from developer. */ -export interface UserImportRecord { - uid: string; - email?: string; - emailVerified?: boolean; - displayName?: string; - phoneNumber?: string; - photoURL?: string; - disabled?: boolean; - metadata?: UserMetadataRequest; - providerData?: Array; - multiFactor?: { - enrolledFactors: SecondFactor[]; - }; - customClaims?: {[key: string]: any}; - passwordHash?: Buffer; - passwordSalt?: Buffer; - tenantId?: string; -} +import UpdateMultiFactorInfoRequest = auth.UpdateMultiFactorInfoRequest; +import UpdatePhoneMultiFactorInfoRequest = auth.UpdatePhoneMultiFactorInfoRequest; +import UserImportRecord = auth.UserImportRecord; +import UserImportOptions = auth.UserImportOptions; +import UserImportResult = auth.UserImportResult; /** Interface representing an Auth second factor in Auth server format. */ export interface AuthFactorInfo { @@ -138,14 +84,6 @@ export interface UploadAccountRequest extends UploadAccountOptions { } -/** Response object for importUsers operation. */ -export interface UserImportResult { - failureCount: number; - successCount: number; - errors: FirebaseArrayIndexError[]; -} - - /** Callback function to validate an UploadAccountUser object. */ export type ValidatorFunction = (data: UploadAccountUser) => void; @@ -155,7 +93,7 @@ export type ValidatorFunction = (data: UploadAccountUser) => void; * @param multiFactorInfo The client format second factor. * @return The corresponding AuthFactorInfo server request format. */ -export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFactor): AuthFactorInfo { +export function convertMultiFactorInfoToServerFormat(multiFactorInfo: UpdateMultiFactorInfoRequest): AuthFactorInfo { let enrolledAt; if (typeof multiFactorInfo.enrollmentTime !== 'undefined') { if (validator.isUTCDateString(multiFactorInfo.enrollmentTime)) { @@ -165,11 +103,11 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFact throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, `The second factor "enrollmentTime" for "${multiFactorInfo.uid}" must be a valid ` + - `UTC date string.`); + 'UTC date string.'); } } // Currently only phone second factors are supported. - if (multiFactorInfo.factorId === 'phone') { + if (isPhoneFactor(multiFactorInfo)) { // If any required field is missing or invalid, validation will still fail later. const authFactorInfo: AuthFactorInfo = { mfaEnrollmentId: multiFactorInfo.uid, @@ -192,6 +130,10 @@ export function convertMultiFactorInfoToServerFormat(multiFactorInfo: SecondFact } } +function isPhoneFactor(multiFactorInfo: UpdateMultiFactorInfoRequest): + multiFactorInfo is UpdatePhoneMultiFactorInfoRequest { + return multiFactorInfo.factorId === 'phone'; +} /** * @param {any} obj The object to check for number field within. @@ -333,7 +275,7 @@ export class UserImportBuilder { const users = this.validatedUsers.map((user) => { return deepCopy(user); }); - return deepExtend({users}, deepCopy(this.validatedOptions)) as UploadAccountRequest; + return deepExtend({ users }, deepCopy(this.validatedOptions)) as UploadAccountRequest; } /** @@ -392,14 +334,14 @@ export class UserImportBuilder { if (!validator.isNonNullObject(options.hash)) { throw new FirebaseAuthError( AuthClientErrorCode.MISSING_HASH_ALGORITHM, - `"hash.algorithm" is missing from the provided "UserImportOptions".`, + '"hash.algorithm" is missing from the provided "UserImportOptions".', ); } if (typeof options.hash.algorithm === 'undefined' || !validator.isNonEmptyString(options.hash.algorithm)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `"hash.algorithm" must be a string matching the list of supported algorithms.`, + '"hash.algorithm" must be a string matching the list of supported algorithms.', ); } @@ -412,7 +354,7 @@ export class UserImportBuilder { if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A non-empty "hash.key" byte buffer must be provided for ` + + 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -448,7 +390,7 @@ export class UserImportBuilder { if (isNaN(rounds) || rounds < 0 || rounds > 120000) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 0 and 120000 must be provided for ` + + 'A valid "hash.rounds" number between 0 and 120000 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -462,7 +404,7 @@ export class UserImportBuilder { if (!validator.isBuffer(options.hash.key)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A "hash.key" byte buffer must be provided for ` + + 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -470,7 +412,7 @@ export class UserImportBuilder { if (isNaN(rounds) || rounds <= 0 || rounds > 8) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 1 and 8 must be provided for ` + + 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -478,7 +420,7 @@ export class UserImportBuilder { if (isNaN(memoryCost) || memoryCost <= 0 || memoryCost > 14) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number between 1 and 14 must be provided for ` + + 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -486,7 +428,7 @@ export class UserImportBuilder { !validator.isBuffer(options.hash.saltSeparator)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, - `"hash.saltSeparator" must be a byte buffer.`, + '"hash.saltSeparator" must be a byte buffer.', ); } populatedOptions = { @@ -509,7 +451,7 @@ export class UserImportBuilder { if (isNaN(cpuMemCost)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number must be provided for ` + + 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -517,7 +459,7 @@ export class UserImportBuilder { if (isNaN(parallelization)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_PARALLELIZATION, - `A valid "hash.parallelization" number must be provided for ` + + 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -525,7 +467,7 @@ export class UserImportBuilder { if (isNaN(blockSize)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, - `A valid "hash.blockSize" number must be provided for ` + + 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } @@ -533,7 +475,7 @@ export class UserImportBuilder { if (isNaN(dkLen)) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, - `A valid "hash.derivedKeyLength" number must be provided for ` + + 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${options.hash.algorithm}.`, ); } diff --git a/src/auth/user-record.ts b/src/auth/user-record.ts index 7d0ebef81b..01092a25da 100644 --- a/src/auth/user-record.ts +++ b/src/auth/user-record.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,10 +15,18 @@ * limitations under the License. */ -import {deepCopy} from '../utils/deep-copy'; -import {isNonNullObject} from '../utils/validator'; +import { deepCopy } from '../utils/deep-copy'; +import { isNonNullObject } from '../utils/validator'; import * as utils from '../utils'; -import {AuthClientErrorCode, FirebaseAuthError} from '../utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../utils/error'; +import { auth } from './index'; + +import MultiFactorInfoInterface = auth.MultiFactorInfo; +import PhoneMultiFactorInfoInterface = auth.PhoneMultiFactorInfo; +import MultiFactorSettings = auth.MultiFactorSettings; +import UserMetadataInterface = auth.UserMetadata; +import UserInfoInterface = auth.UserInfo; +import UserRecordInterface = auth.UserRecord; /** * 'REDACTED', encoded as a base64 string. @@ -42,64 +51,6 @@ function parseDate(time: any): string | null { return null; } -/** - * Interface representing base properties of a user enrolled second factor for a - * `CreateRequest`. - */ -export interface CreateMultiFactorInfoRequest { - displayName?: string; - factorId: string; -} - -/** - * Interface representing a phone specific user enrolled second factor for a - * `CreateRequest`. - */ -export interface CreatePhoneMultiFactorInfoRequest extends CreateMultiFactorInfoRequest { - phoneNumber: string; -} - -/** - * Interface representing common properties of a user enrolled second factor - * for an `UpdateRequest`. - */ -export interface UpdateMultiFactorInfoRequest { - uid?: string; - displayName?: string; - enrollmentTime?: string; - factorId: string; -} - -/** - * Interface representing a phone specific user enrolled second factor - * for an `UpdateRequest`. - */ -export interface UpdatePhoneMultiFactorInfoRequest extends UpdateMultiFactorInfoRequest { - phoneNumber: string; -} - -/** Parameters for update user operation */ -export interface UpdateRequest { - disabled?: boolean; - displayName?: string | null; - email?: string; - emailVerified?: boolean; - password?: string; - phoneNumber?: string | null; - photoURL?: string | null; - multiFactor?: { - enrolledFactors: UpdateMultiFactorInfoRequest[] | null; - }; -} - -/** Parameters for create user operation */ -export interface CreateRequest extends UpdateRequest { - uid?: string; - multiFactor?: { - enrolledFactors: CreateMultiFactorInfoRequest[]; - }; -} - export interface MultiFactorInfoResponse { mfaEnrollmentId: string; displayName?: string; @@ -138,19 +89,18 @@ export interface GetAccountInfoUserResponse { [key: string]: any; } -/** Enums for multi-factor identifiers. */ -export enum MultiFactorId { +enum MultiFactorId { Phone = 'phone', } /** * Abstract class representing a multi-factor info interface. */ -export abstract class MultiFactorInfo { +export abstract class MultiFactorInfo implements MultiFactorInfoInterface { public readonly uid: string; - public readonly displayName: string | null; - public readonly factorId: MultiFactorId; - public readonly enrollmentTime: string; + public readonly displayName?: string; + public readonly factorId: string; + public readonly enrollmentTime?: string; /** * Initializes the MultiFactorInfo associated subclass using the server side. @@ -197,7 +147,7 @@ export abstract class MultiFactorInfo { * @return The multi-factor ID associated with the provided response. If the response is * not associated with any known multi-factor ID, null is returned. */ - protected abstract getFactorId(response: MultiFactorInfoResponse): MultiFactorId | null; + protected abstract getFactorId(response: MultiFactorInfoResponse): string | null; /** * Initializes the MultiFactorInfo object using the provided server response. @@ -213,7 +163,7 @@ export abstract class MultiFactorInfo { } utils.addReadonlyGetter(this, 'uid', response.mfaEnrollmentId); utils.addReadonlyGetter(this, 'factorId', factorId); - utils.addReadonlyGetter(this, 'displayName', response.displayName || null); + utils.addReadonlyGetter(this, 'displayName', response.displayName); // Encoded using [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. // For example, "2017-01-15T01:30:15.01Z". // This can be parsed directly via Date constructor. @@ -228,7 +178,7 @@ export abstract class MultiFactorInfo { } /** Class representing a phone MultiFactorInfo object. */ -export class PhoneMultiFactorInfo extends MultiFactorInfo { +export class PhoneMultiFactorInfo extends MultiFactorInfo implements PhoneMultiFactorInfoInterface { public readonly phoneNumber: string; /** @@ -258,14 +208,14 @@ export class PhoneMultiFactorInfo extends MultiFactorInfo { * @return The multi-factor ID associated with the provided response. If the response is * not associated with any known multi-factor ID, null is returned. */ - protected getFactorId(response: MultiFactorInfoResponse): MultiFactorId | null { + protected getFactorId(response: MultiFactorInfoResponse): string | null { return (response && response.phoneInfo) ? MultiFactorId.Phone : null; } } /** Class representing multi-factor related properties of a user. */ -export class MultiFactor { - public readonly enrolledFactors: ReadonlyArray; +export class MultiFactor implements MultiFactorSettings { + public enrolledFactors: MultiFactorInfo[]; /** * Initializes the MultiFactor object using the server side or JWT format response. @@ -308,7 +258,7 @@ export class MultiFactor { * endpoint. * @constructor */ -export class UserMetadata { +export class UserMetadata implements UserMetadataInterface { public readonly creationTime: string; public readonly lastSignInTime: string; @@ -347,7 +297,7 @@ export class UserMetadata { * endpoint. * @constructor */ -export class UserInfo { +export class UserInfo implements UserInfoInterface { public readonly uid: string; public readonly displayName: string; public readonly email: string; @@ -392,7 +342,7 @@ export class UserInfo { * endpoint. * @constructor */ -export class UserRecord { +export class UserRecord implements UserRecordInterface { public readonly uid: string; public readonly email: string; public readonly emailVerified: boolean; diff --git a/src/auth/credential.ts b/src/credential/credential-internal.ts similarity index 96% rename from src/auth/credential.ts rename to src/credential/credential-internal.ts index 43de4d3ed3..a266a42ceb 100644 --- a/src/auth/credential.ts +++ b/src/credential/credential-internal.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,16 +14,18 @@ * limitations under the License. */ -// Use untyped import syntax for Node built-ins import fs = require('fs'); import os = require('os'); import path = require('path'); -import {AppErrorCodes, FirebaseAppError} from '../utils/error'; -import {HttpClient, HttpRequestConfig, HttpError, HttpResponse} from '../utils/api-request'; -import {Agent} from 'http'; +import { Agent } from 'http'; +import { credential, GoogleOAuthAccessToken } from './index'; +import { AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { HttpClient, HttpRequestConfig, HttpError, HttpResponse } from '../utils/api-request'; import * as util from '../utils/validator'; +import Credential = credential.Credential; + const GOOGLE_TOKEN_AUDIENCE = 'https://accounts.google.com/o/oauth2/token'; const GOOGLE_AUTH_TOKEN_HOST = 'accounts.google.com'; const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; @@ -53,23 +55,6 @@ const REFRESH_TOKEN_PATH = '/oauth2/v4/token'; const ONE_HOUR_IN_SECONDS = 60 * 60; const JWT_ALGORITHM = 'RS256'; -/** - * Interface for Google OAuth 2.0 access tokens. - */ -export interface GoogleOAuthAccessToken { - /* tslint:disable:variable-name */ - access_token: string; - expires_in: number; - /* tslint:enable:variable-name */ -} - -/** - * Interface for things that generate access tokens. - */ -export interface Credential { - getAccessToken(): Promise; -} - /** * Implementation of Credential that uses a service account. */ @@ -347,6 +332,21 @@ class RefreshToken { } } + +/** + * Checks if the given credential was loaded via the application default credentials mechanism. This + * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential + * instances that were loaded from well-known files or environment variables, rather than being explicitly + * instantiated. + * + * @param credential The credential instance to check. + */ +export function isApplicationDefault(credential?: Credential): boolean { + return credential instanceof ComputeEngineCredential || + (credential instanceof ServiceAccountCredential && credential.implicit) || + (credential instanceof RefreshTokenCredential && credential.implicit); +} + export function getApplicationDefault(httpAgent?: Agent): Credential { if (process.env.GOOGLE_APPLICATION_CREDENTIALS) { return credentialFromFile(process.env.GOOGLE_APPLICATION_CREDENTIALS, httpAgent); @@ -363,20 +363,6 @@ export function getApplicationDefault(httpAgent?: Agent): Credential { return new ComputeEngineCredential(httpAgent); } -/** - * Checks if the given credential was loaded via the application default credentials mechanism. This - * includes all ComputeEngineCredential instances, and the ServiceAccountCredential and RefreshTokenCredential - * instances that were loaded from well-known files or environment variables, rather than being explicitly - * instantiated. - * - * @param credential The credential instance to check. - */ -export function isApplicationDefault(credential?: Credential): boolean { - return credential instanceof ComputeEngineCredential || - (credential instanceof ServiceAccountCredential && credential.implicit) || - (credential instanceof RefreshTokenCredential && credential.implicit); -} - /** * Copies the specified property from one object to another. * diff --git a/src/credential/credential.ts b/src/credential/credential.ts new file mode 100644 index 0000000000..a2b4413b73 --- /dev/null +++ b/src/credential/credential.ts @@ -0,0 +1,49 @@ +/*! + * @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 { + ServiceAccountCredential, RefreshTokenCredential, getApplicationDefault +} from './credential-internal'; +import { credential } from './index'; + +let globalAppDefaultCred: credential.Credential; +const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; +const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; + +export const applicationDefault: typeof credential.applicationDefault = (httpAgent?) => { + if (typeof globalAppDefaultCred === 'undefined') { + globalAppDefaultCred = getApplicationDefault(httpAgent); + } + return globalAppDefaultCred; +} + +export const cert: typeof credential.cert = (serviceAccountPathOrObject, httpAgent?) => { + const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); + if (!(stringifiedServiceAccount in globalCertCreds)) { + globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent); + } + return globalCertCreds[stringifiedServiceAccount]; +} + +export const refreshToken: typeof credential.refreshToken = (refreshTokenPathOrObject, httpAgent) => { + const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); + if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { + globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential( + refreshTokenPathOrObject, httpAgent); + } + return globalRefreshTokenCreds[stringifiedRefreshToken]; +} diff --git a/src/credential/index.ts b/src/credential/index.ts new file mode 100644 index 0000000000..dfd27819ed --- /dev/null +++ b/src/credential/index.ts @@ -0,0 +1,177 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Agent } from 'http'; + +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; +} + +/* 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 + * {@link credential `admin.credential`}. + */ + export interface Credential { + /** + * Returns a Google OAuth2 access token object used to authenticate with + * Firebase services. + * + * This object contains the following properties: + * * `access_token` (`string`): The actual Google OAuth2 access token. + * * `expires_in` (`number`): The number of seconds from when the token was + * issued that it expires. + * + * @return A Google OAuth2 access token object. + */ + getAccessToken(): Promise; + } + + /** + * Returns a credential created from the + * {@link + * https://developers.google.com/identity/protocols/application-default-credentials + * Google Application Default Credentials} + * that grants admin access to Firebase services. This credential can be used + * in the call to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * Google Application Default Credentials are available on any Google + * infrastructure, such as Google App Engine and Google Compute Engine. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * admin.initializeApp({ + * credential: admin.credential.applicationDefault(), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return {!admin.credential.Credential} A credential authenticated via Google + * Application Default Credentials that can be used to initialize an app. + */ + export declare function applicationDefault(httpAgent?: Agent): Credential; + + /** + * Returns a credential created from the provided service account that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a service account key JSON file + * var serviceAccount = require("path/to/serviceAccountKey.json"); + * admin.initializeApp({ + * credential: admin.credential.cert(serviceAccount), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @example + * ```javascript + * // Providing a service account object inline + * admin.initializeApp({ + * credential: admin.credential.cert({ + * projectId: "", + * clientEmail: "foo@.iam.gserviceaccount.com", + * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" + * }), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param serviceAccountPathOrObject The path to a service + * account key JSON file or an object representing a service account key. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export declare function cert( + serviceAccountPathOrObject: string | ServiceAccount, httpAgent?: Agent): Credential; + + /** + * Returns a credential created from the provided refresh token that grants + * admin access to Firebase services. This credential can be used in the call + * to + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * See + * {@link + * https://firebase.google.com/docs/admin/setup#initialize_the_sdk + * Initialize the SDK} + * for more details. + * + * @example + * ```javascript + * // Providing a path to a refresh token JSON file + * var refreshToken = require("path/to/refreshToken.json"); + * admin.initializeApp({ + * credential: admin.credential.refreshToken(refreshToken), + * databaseURL: "https://.firebaseio.com" + * }); + * ``` + * + * @param refreshTokenPathOrObject The path to a Google + * OAuth2 refresh token JSON file or an object representing a Google OAuth2 + * refresh token. + * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when retrieving access tokens from Google token servers. + * + * @return A credential authenticated via the + * provided service account that can be used to initialize an app. + */ + export declare function refreshToken( + refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential; +} diff --git a/src/database.d.ts b/src/database.d.ts deleted file mode 100644 index 1bdf425ea1..0000000000 --- a/src/database.d.ts +++ /dev/null @@ -1,1664 +0,0 @@ -import * as _admin from './index.d'; - -/* eslint-disable @typescript-eslint/ban-types */ - -export namespace admin.database { - - /** - * The Firebase Realtime Database service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.database()`](admin.database#database). - * - * See - * {@link - * https://firebase.google.com/docs/database/admin/start/ - * Introduction to the Admin Database API} - * for a full guide on how to use the Firebase Realtime Database service. - */ - interface Database { - app: _admin.app.App; - - /** - * Disconnects from the server (all Database operations will be completed - * offline). - * - * The client automatically maintains a persistent connection to the Database - * server, which will remain active indefinitely and reconnect when - * disconnected. However, the `goOffline()` and `goOnline()` methods may be used - * to control the client connection in cases where a persistent connection is - * undesirable. - * - * While offline, the client will no longer receive data updates from the - * Database. However, all Database operations performed locally will continue to - * immediately fire events, allowing your application to continue behaving - * normally. Additionally, each operation performed locally will automatically - * be queued and retried upon reconnection to the Database server. - * - * To reconnect to the Database and begin receiving remote events, see - * `goOnline()`. - * - * @example - * ```javascript - * admin.database().goOffline(); - * ``` - */ - goOffline(): void; - - /** - * Reconnects to the server and synchronizes the offline Database state - * with the server state. - * - * This method should be used after disabling the active connection with - * `goOffline()`. Once reconnected, the client will transmit the proper data - * and fire the appropriate events so that your client "catches up" - * automatically. - * - * @example - * ```javascript - * admin.database().goOnline(); - * ``` - */ - goOnline(): void; - - /** - * Returns a `Reference` representing the location in the Database - * corresponding to the provided path. Also can be invoked with an existing - * `Reference` as the argument. In that case returns a new `Reference` - * pointing to the same location. If no path argument is - * provided, returns a `Reference` that represents the root of the Database. - * - * @example - * ```javascript - * // Get a reference to the root of the Database - * var rootRef = admin.database.ref(); - * ``` - * - * @example - * ```javascript - * // Get a reference to the /users/ada node - * var adaRef = admin.database().ref("users/ada"); - * // The above is shorthand for the following operations: - * //var rootRef = admin.database().ref(); - * //var adaRef = rootRef.child("users/ada"); - * ``` - * - * @example - * ```javascript - * var adaRef = admin.database().ref("users/ada"); - * // Get a new reference pointing to the same location. - * var anotherAdaRef = admin.database().ref(adaRef); - * ``` - * - * - * @param path Optional path representing - * the location the returned `Reference` will point. Alternatively, a - * `Reference` object to copy. If not provided, the returned `Reference` will - * point to the root of the Database. - * @return If a path is provided, a `Reference` - * pointing to the provided path. Otherwise, a `Reference` pointing to the - * root of the Database. - */ - ref(path?: string | admin.database.Reference): admin.database.Reference; - - /** - * Returns a `Reference` representing the location in the Database - * corresponding to the provided Firebase URL. - * - * An exception is thrown if the URL is not a valid Firebase Database URL or it - * has a different domain than the current `Database` instance. - * - * Note that all query parameters (`orderBy`, `limitToLast`, etc.) are ignored - * and are not applied to the returned `Reference`. - * - * @example - * ```javascript - * // Get a reference to the root of the Database - * var rootRef = admin.database().ref("https://.firebaseio.com"); - * ``` - * - * @example - * ```javascript - * // Get a reference to the /users/ada node - * var adaRef = admin.database().ref("https://.firebaseio.com/users/ada"); - * ``` - * - * @param url The Firebase URL at which the returned `Reference` will - * point. - * @return A `Reference` pointing to the provided Firebase URL. - */ - refFromURL(url: string): admin.database.Reference; - - /** - * Gets the currently applied security rules as a string. The return value consists of - * the rules source including comments. - * - * @return A promise fulfilled with the rules as a raw string. - */ - getRules(): Promise; - - /** - * Gets the currently applied security rules as a parsed JSON object. Any comments in - * the original source are stripped away. - * - * @return A promise fulfilled with the parsed rules object. - */ - getRulesJSON(): Promise; - - /** - * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is - * specified as a string or a Buffer, it may include comments. - * - * @param source Source of the rules to apply. Must not be `null` or empty. - * @return Resolves when the rules are set on the Realtime Database. - */ - setRules(source: string | Buffer | object): Promise; - } - - /** - * A `DataSnapshot` contains data from a Database location. - * - * Any time you read data from the Database, you receive the data as a - * `DataSnapshot`. A `DataSnapshot` is passed to the event callbacks you attach - * with `on()` or `once()`. You can extract the contents of the snapshot as a - * JavaScript object by calling the `val()` method. Alternatively, you can - * traverse into the snapshot by calling `child()` to return child snapshots - * (which you could then call `val()` on). - * - * A `DataSnapshot` is an efficiently generated, immutable copy of the data at - * a Database location. It cannot be modified and will never change (to modify - * data, you always call the `set()` method on a `Reference` directly). - */ - interface DataSnapshot { - key: string | null; - ref: admin.database.Reference; - - /** - * Gets another `DataSnapshot` for the location at the specified relative path. - * - * Passing a relative path to the `child()` method of a DataSnapshot returns - * another `DataSnapshot` for the location at the specified relative path. The - * relative path can either be a simple child name (for example, "ada") or a - * deeper, slash-separated path (for example, "ada/name/first"). If the child - * location has no data, an empty `DataSnapshot` (that is, a `DataSnapshot` - * whose value is `null`) is returned. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Test for the existence of certain keys within a DataSnapshot - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var name = snapshot.child("name").val(); // {first:"Ada",last:"Lovelace"} - * var firstName = snapshot.child("name/first").val(); // "Ada" - * var lastName = snapshot.child("name").child("last").val(); // "Lovelace" - * var age = snapshot.child("age").val(); // null - * }); - * ``` - * - * @param path A relative path to the location of child data. - * @return `DataSnapshot` for the location at the specified relative path. - */ - child(path: string): admin.database.DataSnapshot; - - /** - * Returns true if this `DataSnapshot` contains any data. It is slightly more - * efficient than using `snapshot.val() !== null`. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Test for the existence of certain keys within a DataSnapshot - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.exists(); // true - * var b = snapshot.child("name").exists(); // true - * var c = snapshot.child("name/first").exists(); // true - * var d = snapshot.child("name/middle").exists(); // false - * }); - * ``` - * - * @return Whether this `DataSnapshot` contains any data. - */ - exists(): boolean; - - /** - * Exports the entire contents of the DataSnapshot as a JavaScript object. - * - * The `exportVal()` method is similar to `val()`, except priority information - * is included (if available), making it suitable for backing up your data. - * - * @return The DataSnapshot's contents as a JavaScript value (Object, - * Array, string, number, boolean, or `null`). - */ - exportVal(): any; - - /** - * Enumerates the top-level children in the `DataSnapshot`. - * - * Because of the way JavaScript objects work, the ordering of data in the - * JavaScript object returned by `val()` is not guaranteed to match the ordering - * on the server nor the ordering of `child_added` events. That is where - * `forEach()` comes in handy. It guarantees the children of a `DataSnapshot` - * will be iterated in their query order. - * - * If no explicit `orderBy*()` method is used, results are returned - * ordered by key (unless priorities are used, in which case, results are - * returned by priority). - * - * @example - * ```javascript - * - * // Assume we have the following data in the Database: - * { - * "users": { - * "ada": { - * "first": "Ada", - * "last": "Lovelace" - * }, - * "alan": { - * "first": "Alan", - * "last": "Turing" - * } - * } - * } - * - * // Loop through users in order with the forEach() method. The callback - * // provided to forEach() will be called synchronously with a DataSnapshot - * // for each child: - * var query = admin.database().ref("users").orderByKey(); - * query.once("value") - * .then(function(snapshot) { - * snapshot.forEach(function(childSnapshot) { - * // key will be "ada" the first time and "alan" the second time - * var key = childSnapshot.key; - * // childData will be the actual contents of the child - * var childData = childSnapshot.val(); - * }); - * }); - * ``` - * - * @example - * ```javascript - * // You can cancel the enumeration at any point by having your callback - * // function return true. For example, the following code sample will only - * // fire the callback function one time: - * var query = admin.database().ref("users").orderByKey(); - * query.once("value") - * .then(function(snapshot) { - * snapshot.forEach(function(childSnapshot) { - * var key = childSnapshot.key; // "ada" - * - * // Cancel enumeration - * return true; - * }); - * }); - * ``` - * - * @param action A function - * that will be called for each child `DataSnapshot`. The callback can return - * true to cancel further enumeration. - * @return True if enumeration was canceled due to your callback - * returning true. - */ - forEach(action: (a: admin.database.DataSnapshot) => boolean | void): boolean; - - /** - * Gets the priority value of the data in this `DataSnapshot`. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @return The the priority value of the data in this `DataSnapshot`. - */ - getPriority(): string | number | null; - - /** - * Returns true if the specified child path has (non-null) data. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * // Determine which child keys in DataSnapshot have data. - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var hasName = snapshot.hasChild("name"); // true - * var hasAge = snapshot.hasChild("age"); // false - * }); - * ``` - * - * @param path A relative path to the location of a potential child. - * @return `true` if data exists at the specified child path; else - * `false`. - */ - hasChild(path: string): boolean; - - /** - * Returns whether or not the `DataSnapshot` has any non-`null` child - * properties. - * - * You can use `hasChildren()` to determine if a `DataSnapshot` has any - * children. If it does, you can enumerate them using `forEach()`. If it - * doesn't, then either this snapshot contains a primitive value (which can be - * retrieved with `val()`) or it is empty (in which case, `val()` will return - * `null`). - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.hasChildren(); // true - * var b = snapshot.child("name").hasChildren(); // true - * var c = snapshot.child("name/first").hasChildren(); // false - * }); - * ``` - * - * @return True if this snapshot has any children; else false. - */ - hasChildren(): boolean; - - /** - * Returns the number of child properties of this `DataSnapshot`. - * - * @example - * ```javascript - * // Assume we have the following data in the Database: - * { - * "name": { - * "first": "Ada", - * "last": "Lovelace" - * } - * } - * - * var ref = admin.database().ref("users/ada"); - * ref.once("value") - * .then(function(snapshot) { - * var a = snapshot.numChildren(); // 1 ("name") - * var b = snapshot.child("name").numChildren(); // 2 ("first", "last") - * var c = snapshot.child("name/first").numChildren(); // 0 - * }); - * ``` - * - * @return The number of child properties of this `DataSnapshot`. - */ - numChildren(): number; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object | null; - - /** - * Extracts a JavaScript value from a `DataSnapshot`. - * - * Depending on the data in a `DataSnapshot`, the `val()` method may return a - * scalar type (string, number, or boolean), an array, or an object. It may also - * return null, indicating that the `DataSnapshot` is empty (contains no data). - * - * @example - * ```javascript - * // Write and then read back a string from the Database. - * ref.set("hello") - * .then(function() { - * return ref.once("value"); - * }) - * .then(function(snapshot) { - * var data = snapshot.val(); // data === "hello" - * }); - * ``` - * - * @example - * ```javascript - * // Write and then read back a JavaScript object from the Database. - * ref.set({ name: "Ada", age: 36 }) - * .then(function() { - * return ref.once("value"); - * }) - * .then(function(snapshot) { - * var data = snapshot.val(); - * // data is { "name": "Ada", "age": 36 } - * // data.name === "Ada" - * // data.age === 36 - * }); - * ``` - * - * @return The DataSnapshot's contents as a JavaScript value (Object, - * Array, string, number, boolean, or `null`). - */ - val(): any; - } - - /** - * The `onDisconnect` class allows you to write or clear data when your client - * disconnects from the Database server. These updates occur whether your - * client disconnects cleanly or not, so you can rely on them to clean up data - * even if a connection is dropped or a client crashes. - * - * The `onDisconnect` class is most commonly used to manage presence in - * applications where it is useful to detect how many clients are connected and - * when other clients disconnect. See - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information. - * - * To avoid problems when a connection is dropped before the requests can be - * transferred to the Database server, these functions should be called before - * any data is written. - * - * Note that `onDisconnect` operations are only triggered once. If you want an - * operation to occur each time a disconnect occurs, you'll need to re-establish - * the `onDisconnect` operations each time you reconnect. - */ - interface OnDisconnect { - - /** - * Cancels all previously queued `onDisconnect()` set or update events for this - * location and all children. - * - * If a write has been queued for this location via a `set()` or `update()` at a - * parent location, the write at this location will be canceled, though all - * other siblings will still be written. - * - * @example - * ```javascript - * var ref = admin.database().ref("onlineState"); - * ref.onDisconnect().set(false); - * // ... sometime later - * ref.onDisconnect().cancel(); - * ``` - * - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return Resolves when synchronization to the server is complete. - */ - cancel(onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is deleted when the client is disconnected - * (due to closing the browser, navigating to a new page, or network issues). - * - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return Resolves when synchronization to the server is complete. - */ - remove(onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is set to the specified value when the - * client is disconnected (due to closing the browser, navigating to a new page, - * or network issues). - * - * `set()` is especially useful for implementing "presence" systems, where a - * value should be changed or cleared when a user disconnects so that they - * appear "offline" to other users. See - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information. - * - * Note that `onDisconnect` operations are only triggered once. If you want an - * operation to occur each time a disconnect occurs, you'll need to re-establish - * the `onDisconnect` operations each time. - * - * @example - * ```javascript - * var ref = admin.database().ref("users/ada/status"); - * ref.onDisconnect().set("I disconnected!"); - * ``` - * - * @param value The value to be written to this location on - * disconnect (can be an object, array, string, number, boolean, or null). - * @param onComplete An optional callback function that - * will be called when synchronization to the database server has completed. - * The callback will be passed a single parameter: null for success, or an - * `Error` object indicating a failure. - * @return A promise that resolves when synchronization to the database is complete. - */ - set(value: any, onComplete?: (a: Error | null) => any): Promise; - - /** - * Ensures the data at this location is set to the specified value and priority - * when the client is disconnected (due to closing the browser, navigating to a - * new page, or network issues). - * - * @param value The value to be written to this location on - * disconnect (can be an object, array, string, number, boolean, or null). - * @param priority - * @param onComplete An optional callback function that is - * called when synchronization to the server has completed. The callback - * will be passed a single parameter: null for success, or an Error object - * indicating a failure. - * @return A promise that resolves when synchronization to the database is complete. - */ - setWithPriority( - value: any, - priority: number | string | null, - onComplete?: (a: Error | null) => any - ): Promise; - - /** - * Writes multiple values at this location when the client is disconnected (due - * to closing the browser, navigating to a new page, or network issues). - * - * The `values` argument contains multiple property-value pairs that will be - * written to the Database together. Each child property can either be a simple - * property (for example, "name") or a relative path (for example, "name/first") - * from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update - * only the referenced properties at the current location (instead of replacing - * all the child properties at the current location). - * - * See {@link https://firebase.google.com/docs/reference/admin/node/admin.database.Reference#update} - * for examples of using the connected version of `update`. - * - * @example - * ```javascript - * var ref = admin.database().ref("users/ada"); - * ref.update({ - * onlineState: true, - * status: "I'm online." - * }); - * ref.onDisconnect().update({ - * onlineState: false, - * status: "I'm offline." - * }); - * ``` - * - * @param values Object containing multiple values. - * @param onComplete An optional callback function that will - * be called when synchronization to the server has completed. The - * callback will be passed a single parameter: null for success, or an Error - * object indicating a failure. - * @return Resolves when synchronization to the - * Database is complete. - */ - update(values: Object, onComplete?: (a: Error | null) => any): Promise; - } - - type EventType = 'value' | 'child_added' | 'child_changed' | 'child_moved' | 'child_removed'; - - /** - * A `Query` sorts and filters the data at a Database location so only a subset - * of the child data is included. This can be used to order a collection of - * data by some attribute (for example, height of dinosaurs) as well as to - * restrict a large list of items (for example, chat messages) down to a number - * suitable for synchronizing to the client. Queries are created by chaining - * together one or more of the filter methods defined here. - * - * Just as with a `Reference`, you can receive data from a `Query` by using the - * `on()` method. You will only receive events and `DataSnapshot`s for the - * subset of the data that matches your query. - * - * See - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data} for more information. - */ - interface Query { - ref: admin.database.Reference; - - /** - * Creates a `Query` with the specified ending point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary - * starting and ending points for your queries. - * - * The ending point is inclusive, so children with exactly the specified value - * will be included in the query. The optional key argument can be used to - * further limit the range of the query. If it is specified, then children that - * have exactly the specified value must also have a key name less than or equal - * to the specified key. - * - * You can read more about `endAt()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find all dinosaurs whose names come before Pterodactyl lexicographically. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByKey().endAt("pterodactyl").on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * ``` - * - * @param value The value to end at. The argument - * type depends on which `orderBy*()` function was used in this query. - * Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to end at, among the children with the - * previously specified priority. This argument is only allowed if ordering by - * priority. - * @return A new `Query` object. - */ - endAt(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * Creates a `Query` that includes children that match the specified value. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows us to choose arbitrary - * starting and ending points for our queries. - * - * The optional key argument can be used to further limit the range of the - * query. If it is specified, then children that have exactly the specified - * value must also have exactly the specified key as their key name. This can be - * used to filter result sets with many matches for the same value. - * - * You can read more about `equalTo()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * // Find all dinosaurs whose height is exactly 25 meters. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").equalTo(25).on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * - * @param value The value to match for. The - * argument type depends on which `orderBy*()` function was used in this - * query. Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at, among the children with the - * previously specified priority. This argument is only allowed if ordering by - * priority. - * @return A new `Query` object. - */ - equalTo(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * Returns whether or not the current and provided queries represent the same - * location, have the same query parameters, and are from the same instance of - * `admin.app.App`. - * - * Two `Reference` objects are equivalent if they represent the same location - * and are from the same instance of `admin.app.App`. - * - * Two `Query` objects are equivalent if they represent the same location, have - * the same query parameters, and are from the same instance of `admin.app.App`. - * Equivalent queries share the same sort order, limits, and starting and - * ending points. - * - * @example - * ```javascript - * var rootRef = admin.database().ref(); - * var usersRef = rootRef.child("users"); - * - * usersRef.isEqual(rootRef); // false - * usersRef.isEqual(rootRef.child("users")); // true - * usersRef.parent.isEqual(rootRef); // true - * ``` - * - * @example - * ```javascript - * var rootRef = admin.database().ref(); - * var usersRef = rootRef.child("users"); - * var usersQuery = usersRef.limitToLast(10); - * - * usersQuery.isEqual(usersRef); // false - * usersQuery.isEqual(usersRef.limitToLast(10)); // true - * usersQuery.isEqual(rootRef.limitToLast(10)); // false - * usersQuery.isEqual(usersRef.orderByKey().limitToLast(10)); // false - * ``` - * - * @param other The query to compare against. - * @return Whether or not the current and provided queries are - * equivalent. - */ - isEqual(other: admin.database.Query | null): boolean; - - /** - * Generates a new `Query` limited to the first specific number of children. - * - * The `limitToFirst()` method is used to set a maximum number of children to be - * synced for a given callback. If we set a limit of 100, we will initially only - * receive up to 100 `child_added` events. If we have fewer than 100 messages - * stored in our Database, a `child_added` event will fire for each message. - * However, if we have over 100 messages, we will only receive a `child_added` - * event for the first 100 ordered messages. As items change, we will receive - * `child_removed` events for each item that drops out of the active list so - * that the total number stays at 100. - * - * You can read more about `limitToFirst()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find the two shortest dinosaurs. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").limitToFirst(2).on("child_added", function(snapshot) { - * // This will be called exactly two times (unless there are less than two - * // dinosaurs in the Database). - * - * // It will also get fired again if one of the first two dinosaurs is - * // removed from the data set, as a new dinosaur will now be the second - * // shortest. - * console.log(snapshot.key); - * }); - * ``` - * - * @param limit The maximum number of nodes to include in this query. - * @return A `Query` object. - */ - limitToFirst(limit: number): admin.database.Query; - - /** - * Generates a new `Query` object limited to the last specific number of - * children. - * - * The `limitToLast()` method is used to set a maximum number of children to be - * synced for a given callback. If we set a limit of 100, we will initially only - * receive up to 100 `child_added` events. If we have fewer than 100 messages - * stored in our Database, a `child_added` event will fire for each message. - * However, if we have over 100 messages, we will only receive a `child_added` - * event for the last 100 ordered messages. As items change, we will receive - * `child_removed` events for each item that drops out of the active list so - * that the total number stays at 100. - * - * You can read more about `limitToLast()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find the two heaviest dinosaurs. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("weight").limitToLast(2).on("child_added", function(snapshot) { - * // This callback will be triggered exactly two times, unless there are - * // fewer than two dinosaurs stored in the Database. It will also get fired - * // for every new, heavier dinosaur that gets added to the data set. - * console.log(snapshot.key); - * }); - * ``` - * - * @param limit The maximum number of nodes to include in this query. - * @return A `Query` object. - */ - limitToLast(limit: number): admin.database.Query; - - /** - * Detaches a callback previously attached with `on()`. - * - * Detach a callback previously attached with `on()`. Note that if `on()` was - * called multiple times with the same eventType and callback, the callback - * will be called multiple times for each event, and `off()` must be called - * multiple times to remove the callback. Calling `off()` on a parent listener - * will not automatically remove listeners registered on child nodes, `off()` - * must also be called on any child listeners to remove the callback. - * - * If a callback is not specified, all callbacks for the specified eventType - * will be removed. Similarly, if no eventType or callback is specified, all - * callbacks for the `Reference` will be removed. - * - * @example - * ```javascript - * var onValueChange = function(dataSnapshot) { ... }; - * ref.on('value', onValueChange); - * ref.child('meta-data').on('child_added', onChildAdded); - * // Sometime later... - * ref.off('value', onValueChange); - * - * // You must also call off() for any child listeners on ref - * // to cancel those callbacks - * ref.child('meta-data').off('child_added', onValueAdded); - * ``` - * - * @example - * ```javascript - * // Or you can save a line of code by using an inline function - * // and on()'s return value. - * var onValueChange = ref.on('value', function(dataSnapshot) { ... }); - * // Sometime later... - * ref.off('value', onValueChange); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback The callback function that was passed to `on()`. - * @param context The context that was passed to `on()`. - */ - off( - eventType?: admin.database.EventType, - callback?: (a: admin.database.DataSnapshot, b?: string | null) => any, - context?: Object | null - ): void; - - /** - * Listens for data changes at a particular location. - * - * This is the primary way to read data from a Database. Your callback - * will be triggered for the initial data and again whenever the data changes. - * Use `off( )` to stop receiving updates. See - * {@link https://firebase.google.com/docs/database/web/retrieve-data - * Retrieve Data on the Web} - * for more details. - * - *

    value event

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

    child_added event

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

    child_removed event

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

    child_changed event

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

    child_moved event

    - * - * This event will be triggered when a child's sort order changes such that its - * position relative to its siblings changes. The `DataSnapshot` passed to the - * callback will be for the data of the child that has moved. It is also passed - * a second argument which is a string containing the key of the previous - * sibling child by sort order (or `null` if it is the first child). - * - * @example - * ```javascript - * // Handle a new value. - * ref.on('value', function(dataSnapshot) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle a new child. - * ref.on('child_added', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child removal. - * ref.on('child_removed', function(oldChildSnapshot) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child data changes. - * ref.on('child_changed', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @example - * ```javascript - * // Handle child ordering changes. - * ref.on('child_moved', function(childSnapshot, prevChildKey) { - * ... - * }); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param callback A callback that fires when the specified event occurs. The callback is - * passed a DataSnapshot. For ordering purposes, "child_added", - * "child_changed", and "child_moved" will also be passed a string containing - * the key of the previous child, by sort order (or `null` if it is the - * first child). - * @param cancelCallbackOrContext An optional - * callback that will be notified if your event subscription is ever canceled - * because your client does not have permission to read this data (or it had - * permission but has now lost it). This callback will be passed an `Error` - * object indicating why the failure occurred. - * @param context If provided, this object will be used as `this` - * when calling your callback(s). - * @return The provided - * callback function is returned unmodified. This is just for convenience if - * you want to pass an inline function to `on()`, but store the callback - * function for later passing to `off()`. - */ - on( - eventType: admin.database.EventType, - callback: (a: admin.database.DataSnapshot, b?: string | null) => any, - cancelCallbackOrContext?: ((a: Error) => any) | Object | null, - context?: Object | null - ): (a: admin.database.DataSnapshot | null, b?: string) => any; - - /** - * Listens for exactly one event of the specified event type, and then stops - * listening. - * - * This is equivalent to calling `on()`, and then calling `off()` inside the - * callback function. See `on()` for details on the event types. - * - * @example - * ```javascript - * // Basic usage of .once() to read the data located at ref. - * ref.once('value') - * .then(function(dataSnapshot) { - * // handle read data. - * }); - * ``` - * - * @param eventType One of the following strings: "value", - * "child_added", "child_changed", "child_removed", or "child_moved." - * @param successCallback A callback that fires when the specified event occurs. The callback is - * passed a `DataSnapshot`. For ordering purposes, "child_added", - * "child_changed", and "child_moved" will also be passed a string containing - * the key of the previous child by sort order (or `null` if it is the - * first child). - * @param failureCallbackOrContext An optional - * callback that will be notified if your client does not have permission to - * read the data. This callback will be passed an `Error` object indicating - * why the failure occurred. - * @param context If provided, this object will be used as `this` - * when calling your callback(s). - * @return {!Promise} - */ - once( - eventType: admin.database.EventType, - successCallback?: (a: admin.database.DataSnapshot, b?: string | null ) => any, - failureCallbackOrContext?: ((a: Error) => void) | Object | null, - context?: Object | null - ): Promise; - - /** - * Generates a new `Query` object ordered by the specified child key. - * - * Queries can only order by one key at a time. Calling `orderByChild()` - * multiple times on the same query is an error. - * - * Firebase queries allow you to order your data by any child key on the fly. - * However, if you know in advance what your indexes will be, you can define - * them via the .indexOn rule in your Security Rules for better performance. See - * the {@link https://firebase.google.com/docs/database/security/indexing-data - * .indexOn} rule for more information. - * - * You can read more about `orderByChild()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").on("child_added", function(snapshot) { - * console.log(snapshot.key + " was " + snapshot.val().height + " m tall"); - * }); - * ``` - * - * @param path - * @return A new `Query` object. - */ - orderByChild(path: string): admin.database.Query; - - /** - * Generates a new `Query` object ordered by key. - * - * Sorts the results of a query by their (ascending) key values. - * - * You can read more about `orderByKey()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByKey().on("child_added", function(snapshot) { - * console.log(snapshot.key); - * }); - * ``` - * - * @return A new `Query` object. - */ - orderByKey(): admin.database.Query; - - /** - * Generates a new `Query` object ordered by priority. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data} for alternatives to priority. - * - * @return A new `Query` object. - */ - orderByPriority(): admin.database.Query; - - /** - * Generates a new `Query` object ordered by value. - * - * If the children of a query are all scalar values (string, number, or - * boolean), you can order the results by their (ascending) values. - * - * You can read more about `orderByValue()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sort_data - * Sort data}. - * - * @example - * ```javascript - * var scoresRef = admin.database().ref("scores"); - * scoresRef.orderByValue().limitToLast(3).on("value", function(snapshot) { - * snapshot.forEach(function(data) { - * console.log("The " + data.key + " score is " + data.val()); - * }); - * }); - * ``` - * - * @return A new `Query` object. - */ - orderByValue(): admin.database.Query; - - /** - * Creates a `Query` with the specified starting point. - * - * Using `startAt()`, `endAt()`, and `equalTo()` allows you to choose arbitrary - * starting and ending points for your queries. - * - * The starting point is inclusive, so children with exactly the specified value - * will be included in the query. The optional key argument can be used to - * further limit the range of the query. If it is specified, then children that - * have exactly the specified value must also have a key name greater than or - * equal to the specified key. - * - * You can read more about `startAt()` in - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#filtering_data - * Filtering data}. - * - * @example - * ```javascript - * // Find all dinosaurs that are at least three meters tall. - * var ref = admin.database().ref("dinosaurs"); - * ref.orderByChild("height").startAt(3).on("child_added", function(snapshot) { - * console.log(snapshot.key) - * }); - * ``` - * - * @param value The value to start at. The argument - * type depends on which `orderBy*()` function was used in this query. - * Specify a value that matches the `orderBy*()` type. When used in - * combination with `orderByKey()`, the value must be a string. - * @param key The child key to start at. This argument is allowed if - * ordering by child, value, or priority. - * @return A new `Query` object. - */ - startAt(value: number | string | boolean | null, key?: string): admin.database.Query; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object; - - /** - * Gets the absolute URL for this location. - * - * The `toString()` method returns a URL that is ready to be put into a browser, - * curl command, or a `admin.database().refFromURL()` call. Since all of those - * expect the URL to be url-encoded, `toString()` returns an encoded URL. - * - * Append '.json' to the returned URL when typed into a browser to download - * JSON-formatted data. If the location is secured (that is, not publicly - * readable), you will get a permission-denied error. - * - * @example - * ```javascript - * // Calling toString() on a root Firebase reference returns the URL where its - * // data is stored within the Database: - * var rootRef = admin.database().ref(); - * var rootUrl = rootRef.toString(); - * // rootUrl === "https://sample-app.firebaseio.com/". - * - * // Calling toString() at a deeper Firebase reference returns the URL of that - * // deep path within the Database: - * var adaRef = rootRef.child('users/ada'); - * var adaURL = adaRef.toString(); - * // adaURL === "https://sample-app.firebaseio.com/users/ada". - * ``` - * - * @return The absolute URL for this location. - * @override - */ - toString(): string; - } - - /** - * A `Reference` represents a specific location in your Database and can be used - * for reading or writing data to that Database location. - * - * You can reference the root or child location in your Database by calling - * `admin.database().ref()` or `admin.database().ref("child/path")`. - * - * Writing is done with the `set()` method and reading can be done with the - * `on()` method. See - * {@link - * https://firebase.google.com/docs/database/web/read-and-write - * Read and Write Data on the Web} - */ - interface Reference extends admin.database.Query { - - /** - * The last part of the `Reference`'s path. - * - * For example, `"ada"` is the key for - * `https://.firebaseio.com/users/ada`. - * - * The key of a root `Reference` is `null`. - * - * @example - * ```javascript - * // The key of a root reference is null - * var rootRef = admin.database().ref(); - * var key = rootRef.key; // key === null - * ``` - * - * @example - * ```javascript - * // The key of any non-root reference is the last token in the path - * var adaRef = admin.database().ref("users/ada"); - * var key = adaRef.key; // key === "ada" - * key = adaRef.child("name/last").key; // key === "last" - * ``` - */ - key: string | null; - - /** - * The parent location of a `Reference`. - * - * The parent of a root `Reference` is `null`. - * - * @example - * ```javascript - * // The parent of a root reference is null - * var rootRef = admin.database().ref(); - * parent = rootRef.parent; // parent === null - * ``` - * - * @example - * ```javascript - * // The parent of any non-root reference is the parent location - * var usersRef = admin.database().ref("users"); - * var adaRef = admin.database().ref("users/ada"); - * // usersRef and adaRef.parent represent the same location - * ``` - */ - parent: admin.database.Reference | null; - - /** - * The root `Reference` of the Database. - * - * @example - * ```javascript - * // The root of a root reference is itself - * var rootRef = admin.database().ref(); - * // rootRef and rootRef.root represent the same location - * ``` - * - * @example - * ```javascript - * // The root of any non-root reference is the root location - * var adaRef = admin.database().ref("users/ada"); - * // rootRef and adaRef.root represent the same location - * ``` - */ - root: admin.database.Reference; - /** @deprecated Removed in next major release to match Web SDK typings. */ - path: string; - - /** - * Gets a `Reference` for the location at the specified relative path. - * - * The relative path can either be a simple child name (for example, "ada") or - * a deeper slash-separated path (for example, "ada/name/first"). - * - * @example - * ```javascript - * var usersRef = admin.database().ref('users'); - * var adaRef = usersRef.child('ada'); - * var adaFirstNameRef = adaRef.child('name/first'); - * var path = adaFirstNameRef.toString(); - * // path is now 'https://sample-app.firebaseio.com/users/ada/name/first' - * ``` - * - * @param path A relative path from this location to the desired child - * location. - * @return The specified child location. - */ - child(path: string): admin.database.Reference; - - /** - * Returns an `OnDisconnect` object - see - * {@link - * https://firebase.google.com/docs/database/web/offline-capabilities - * Enabling Offline Capabilities in JavaScript} for more information on how - * to use it. - * - * @return An `OnDisconnect` object . - */ - onDisconnect(): admin.database.OnDisconnect; - - /** - * Generates a new child location using a unique key and returns its - * `Reference`. - * - * This is the most common pattern for adding data to a collection of items. - * - * If you provide a value to `push()`, the value will be written to the - * generated location. If you don't pass a value, nothing will be written to the - * Database and the child will remain empty (but you can use the `Reference` - * elsewhere). - * - * The unique key generated by `push()` are ordered by the current time, so the - * resulting list of items will be chronologically sorted. The keys are also - * designed to be unguessable (they contain 72 random bits of entropy). - * - * - * See - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#append_to_a_list_of_data - * Append to a list of data} - *
    See - * {@link - * https://firebase.googleblog.com/2015/02/the-2120-ways-to-ensure-unique_68.html - * The 2^120 Ways to Ensure Unique Identifiers} - * - * @example - * ```javascript - * var messageListRef = admin.database().ref('message_list'); - * var newMessageRef = messageListRef.push(); - * newMessageRef.set({ - * user_id: 'ada', - * text: 'The Analytical Engine weaves algebraical patterns just as the Jacquard loom weaves flowers and leaves.' - * }); - * // We've appended a new message to the message_list location. - * var path = newMessageRef.toString(); - * // path will be something like - * // 'https://sample-app.firebaseio.com/message_list/-IKo28nwJLH0Nc5XeFmj' - * ``` - * - * @param value Optional value to be written at the generated location. - * @param onComplete Callback called when write to server is - * complete. - * @return Combined `Promise` and - * `Reference`; resolves when write is complete, but can be used immediately - * as the `Reference` to the child location. - */ - push(value?: any, onComplete?: (a: Error | null) => any): admin.database.ThenableReference; - - /** - * Removes the data at this Database location. - * - * Any data at child locations will also be deleted. - * - * The effect of the remove will be visible immediately and the corresponding - * event 'value' will be triggered. Synchronization of the remove to the - * Firebase servers will also be started, and the returned Promise will resolve - * when complete. If provided, the onComplete callback will be called - * asynchronously after synchronization has finished. - * - * @example - * ```javascript - * var adaRef = admin.database().ref('users/ada'); - * adaRef.remove() - * .then(function() { - * console.log("Remove succeeded.") - * }) - * .catch(function(error) { - * console.log("Remove failed: " + error.message) - * }); - * ``` - * - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when remove on server is complete. - */ - remove(onComplete?: (a: Error | null) => any): Promise; - - /** - * Writes data to this Database location. - * - * This will overwrite any data at this location and all child locations. - * - * The effect of the write will be visible immediately, and the corresponding - * events ("value", "child_added", etc.) will be triggered. Synchronization of - * the data to the Firebase servers will also be started, and the returned - * Promise will resolve when complete. If provided, the `onComplete` callback - * will be called asynchronously after synchronization has finished. - * - * Passing `null` for the new value is equivalent to calling `remove()`; namely, - * all data at this location and all child locations will be deleted. - * - * `set()` will remove any priority stored at this location, so if priority is - * meant to be preserved, you need to use `setWithPriority()` instead. - * - * Note that modifying data with `set()` will cancel any pending transactions - * at that location, so extreme care should be taken if mixing `set()` and - * `transaction()` to modify the same data. - * - * A single `set()` will generate a single "value" event at the location where - * the `set()` was performed. - * - * @example - * ```javascript - * var adaNameRef = admin.database().ref('users/ada/name'); - * adaNameRef.child('first').set('Ada'); - * adaNameRef.child('last').set('Lovelace'); - * // We've written 'Ada' to the Database location storing Ada's first name, - * // and 'Lovelace' to the location storing her last name. - * ``` - * - * @example - * ```javascript - * adaNameRef.set({ first: 'Ada', last: 'Lovelace' }); - * // Exact same effect as the previous example, except we've written - * // Ada's first and last name simultaneously. - * ``` - * - * @example - * ```javascript - * adaNameRef.set({ first: 'Ada', last: 'Lovelace' }) - * .then(function() { - * console.log('Synchronization succeeded'); - * }) - * .catch(function(error) { - * console.log('Synchronization failed'); - * }); - * // Same as the previous example, except we will also log a message - * // when the data has finished synchronizing. - * ``` - * - * @param value The value to be written (string, number, boolean, object, - * array, or null). - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when write to server is complete. - */ - set(value: any, onComplete?: (a: Error | null) => any): Promise; - - /** - * Sets a priority for the data at this Database location. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @param priority - * @param onComplete - * @return - */ - setPriority( - priority: string | number | null, - onComplete: (a: Error | null) => any - ): Promise; - - /** - * Writes data the Database location. Like `set()` but also specifies the - * priority for that data. - * - * Applications need not use priority but can order collections by - * ordinary properties (see - * {@link - * https://firebase.google.com/docs/database/web/lists-of-data#sorting_and_filtering_data - * Sorting and filtering data}). - * - * @param newVal - * @param newPriority - * @param onComplete - * @return - */ - setWithPriority( - newVal: any, newPriority: string | number | null, - onComplete?: (a: Error | null) => any - ): Promise; - - /** - * Atomically modifies the data at this location. - * - * Atomically modify the data at this location. Unlike a normal `set()`, which - * just overwrites the data regardless of its previous value, `transaction()` is - * used to modify the existing value to a new value, ensuring there are no - * conflicts with other clients writing to the same location at the same time. - * - * To accomplish this, you pass `transaction()` an update function which is used - * to transform the current value into a new value. If another client writes to - * the location before your new value is successfully written, your update - * function will be called again with the new current value, and the write will - * be retried. This will happen repeatedly until your write succeeds without - * conflict or you abort the transaction by not returning a value from your - * update function. - * - * Note: Modifying data with `set()` will cancel any pending transactions at - * that location, so extreme care should be taken if mixing `set()` and - * `transaction()` to update the same data. - * - * Note: When using transactions with Security and Firebase Rules in place, be - * aware that a client needs `.read` access in addition to `.write` access in - * order to perform a transaction. This is because the client-side nature of - * transactions requires the client to read the data in order to transactionally - * update it. - * - * @example - * ```javascript - * // Increment Ada's rank by 1. - * var adaRankRef = admin.database().ref('users/ada/rank'); - * adaRankRef.transaction(function(currentRank) { - * // If users/ada/rank has never been set, currentRank will be `null`. - * return currentRank + 1; - * }); - * ``` - * - * @example - * ```javascript - * // Try to create a user for ada, but only if the user id 'ada' isn't - * // already taken - * var adaRef = admin.database().ref('users/ada'); - * adaRef.transaction(function(currentData) { - * if (currentData === null) { - * return { name: { first: 'Ada', last: 'Lovelace' } }; - * } else { - * console.log('User ada already exists.'); - * return; // Abort the transaction. - * } - * }, function(error, committed, snapshot) { - * if (error) { - * console.log('Transaction failed abnormally!', error); - * } else if (!committed) { - * console.log('We aborted the transaction (because ada already exists).'); - * } else { - * console.log('User ada added!'); - * } - * console.log("Ada's data: ", snapshot.val()); - * }); - * ``` - * - * @param transactionUpdate A developer-supplied function which - * will be passed the current data stored at this location (as a JavaScript - * object). The function should return the new value it would like written (as - * a JavaScript object). If `undefined` is returned (i.e. you return with no - * arguments) the transaction will be aborted and the data at this location - * will not be modified. - * @param onComplete A callback - * function that will be called when the transaction completes. The callback - * is passed three arguments: a possibly-null `Error`, a `boolean` indicating - * whether the transaction was committed, and a `DataSnapshot` indicating the - * final result. If the transaction failed abnormally, the first argument will - * be an `Error` object indicating the failure cause. If the transaction - * finished normally, but no data was committed because no data was returned - * from `transactionUpdate`, then second argument will be false. If the - * transaction completed and committed data to Firebase, the second argument - * will be true. Regardless, the third argument will be a `DataSnapshot` - * containing the resulting data in this location. - * @param applyLocally By default, events are raised each time the - * transaction update function runs. So if it is run multiple times, you may - * see intermediate states. You can set this to false to suppress these - * intermediate states and instead wait until the transaction has completed - * before events are raised. - * @return Returns a Promise that can optionally be used instead of the `onComplete` - * callback to handle success and failure. - */ - transaction( - transactionUpdate: (a: any) => any, - onComplete?: (a: Error | null, b: boolean, c: admin.database.DataSnapshot | null) => any, - applyLocally?: boolean - ): Promise<{ - committed: boolean; - snapshot: admin.database.DataSnapshot | null; - }>; - - /** - * Writes multiple values to the Database at once. - * - * The `values` argument contains multiple property-value pairs that will be - * written to the Database together. Each child property can either be a simple - * property (for example, "name") or a relative path (for example, - * "name/first") from the current location to the data to update. - * - * As opposed to the `set()` method, `update()` can be use to selectively update - * only the referenced properties at the current location (instead of replacing - * all the child properties at the current location). - * - * The effect of the write will be visible immediately, and the corresponding - * events ('value', 'child_added', etc.) will be triggered. Synchronization of - * the data to the Firebase servers will also be started, and the returned - * Promise will resolve when complete. If provided, the `onComplete` callback - * will be called asynchronously after synchronization has finished. - * - * A single `update()` will generate a single "value" event at the location - * where the `update()` was performed, regardless of how many children were - * modified. - * - * Note that modifying data with `update()` will cancel any pending - * transactions at that location, so extreme care should be taken if mixing - * `update()` and `transaction()` to modify the same data. - * - * Passing `null` to `update()` will remove the data at this location. - * - * See - * {@link - * https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html - * Introducing multi-location updates and more}. - * - * @example - * ```javascript - * var adaNameRef = admin.database().ref('users/ada/name'); - * // Modify the 'first' and 'last' properties, but leave other data at - * // adaNameRef unchanged. - * adaNameRef.update({ first: 'Ada', last: 'Lovelace' }); - * ``` - * - * @param values Object containing multiple values. - * @param onComplete Callback called when write to server is - * complete. - * @return Resolves when update on server is complete. - */ - update(values: Object, onComplete?: (a: Error | null) => any): Promise; - } - - /** - * @extends {Reference} - */ - interface ThenableReference extends admin.database.Reference, Promise { } - - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - function enableLogging(logger?: boolean | ((message: string) => any), persistent?: boolean): any; -} - -/* eslint-disable @typescript-eslint/no-unused-vars */ -export namespace admin.database.ServerValue { - - /** - * A placeholder value for auto-populating the current timestamp (time - * since the Unix epoch, in milliseconds) as determined by the Firebase - * servers. - * - * @example - * ```javascript - * var sessionsRef = firebase.database().ref("sessions"); - * sessionsRef.push({ - * startedAt: firebase.database.ServerValue.TIMESTAMP - * }); - * ``` - */ - const TIMESTAMP: Object; - - /** - * Returns a placeholder value that can be used to atomically increment the - * current database value by the provided delta. - * - * @param delta the amount to modify the current value atomically. - * @return a placeholder value for modifying data atomically server-side. - */ - function increment(delta: number): Object; -} diff --git a/src/database/database.ts b/src/database/database-internal.ts similarity index 58% rename from src/database/database.ts rename to src/database/database-internal.ts index 3da2a356ba..9ad9e5bb51 100644 --- a/src/database/database.ts +++ b/src/database/database-internal.ts @@ -1,51 +1,44 @@ -import {URL} from 'url'; +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { URL } from 'url'; import * as path from 'path'; -import {FirebaseApp} from '../firebase-app'; -import {FirebaseDatabaseError, AppErrorCodes, FirebaseAppError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {Database} from '@firebase/database'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseDatabaseError, AppErrorCodes, FirebaseAppError } from '../utils/error'; +import { Database as DatabaseImpl } from '@firebase/database'; +import { database } from './index'; import * as validator from '../utils/validator'; import { AuthorizedHttpClient, HttpRequestConfig, HttpError } from '../utils/api-request'; +import { getSdkVersion } from '../utils/index'; +import Database = database.Database; -/** - * Internals of a Database instance. - */ -class DatabaseInternals implements FirebaseServiceInternalsInterface { - - public databases: { - [dbUrl: string]: Database; - } = {}; - - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - for (const dbUrl of Object.keys(this.databases)) { - const db: Database = this.databases[dbUrl]; - db.INTERNAL.delete(); - } - return Promise.resolve(undefined); - } -} - -declare module '@firebase/database' { - interface Database { - getRules(): Promise; - getRulesJSON(): Promise; - setRules(source: string | Buffer | object): Promise; - } -} - -export class DatabaseService implements FirebaseServiceInterface { +const TOKEN_REFRESH_THRESHOLD_MILLIS = 5 * 60 * 1000; - public readonly INTERNAL: DatabaseInternals = new DatabaseInternals(); +export class DatabaseService { private readonly appInternal: FirebaseApp; + private tokenListener: (token: string) => void; + private tokenRefreshTimeout: NodeJS.Timeout; + + private databases: { + [dbUrl: string]: Database; + } = {}; constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { @@ -57,10 +50,29 @@ export class DatabaseService implements FirebaseServiceInterface { this.appInternal = app; } + /** + * @internal + */ + public delete(): Promise { + if (this.tokenListener) { + this.appInternal.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. + * @return The app associated with this DatabaseService instance. */ get app(): FirebaseApp { return this.appInternal; @@ -75,11 +87,10 @@ export class DatabaseService implements FirebaseServiceInterface { }); } - let db: Database = this.INTERNAL.databases[dbUrl]; + let db: Database = this.databases[dbUrl]; if (typeof db === 'undefined') { const rtdb = require('@firebase/database'); // eslint-disable-line @typescript-eslint/no-var-requires - const { version } = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires - db = rtdb.initStandalone(this.appInternal, dbUrl, version).instance; + db = rtdb.initStandalone(this.appInternal, dbUrl, getSdkVersion()).instance; const rulesClient = new DatabaseRulesClient(this.app, dbUrl); db.getRules = () => { @@ -88,15 +99,46 @@ export class DatabaseService implements FirebaseServiceInterface { db.getRulesJSON = () => { return rulesClient.getRulesJSON(); }; - db.setRules = (source) => { + db.setRules = (source: string) => { return rulesClient.setRules(source); }; - this.INTERNAL.databases[dbUrl] = db; + this.databases[dbUrl] = db; } + + if (!this.tokenListener) { + this.tokenListener = this.onTokenChange.bind(this); + this.appInternal.INTERNAL.addAuthTokenListener(this.tokenListener); + } + return db; } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + private onTokenChange(_: string): void { + const token = this.appInternal.INTERNAL.getCachedToken(); + if (token) { + const delayMillis = token.expirationTime - TOKEN_REFRESH_THRESHOLD_MILLIS - Date.now(); + // If the new token is set to expire soon (unlikely), do nothing. Somebody will eventually + // notice and refresh the token, at which point this callback will fire again. + if (delayMillis > 0) { + this.scheduleTokenRefresh(delayMillis); + } + } + } + + private scheduleTokenRefresh(delayMillis: number): void { + clearTimeout(this.tokenRefreshTimeout); + this.tokenRefreshTimeout = setTimeout(() => { + this.appInternal.INTERNAL.getToken(/*forceRefresh=*/ true) + .catch(() => { + // Ignore the error since this might just be an intermittent failure. If we really cannot + // refresh the token, an error will be logged once the existing token expires and we try + // to fetch a fresh one. + }); + }, delayMillis); + } + private ensureUrl(url?: string): string { if (typeof url !== 'undefined') { return url; @@ -121,7 +163,13 @@ class DatabaseRulesClient { private readonly httpClient: AuthorizedHttpClient; constructor(app: FirebaseApp, dbUrl: string) { - const parsedUrl = new URL(dbUrl); + let parsedUrl = new URL(dbUrl); + const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST; + if (emulatorHost) { + const namespace = extractNamespace(parsedUrl); + parsedUrl = new URL(`http://${emulatorHost}?ns=${namespace}`); + } + parsedUrl.pathname = path.join(parsedUrl.pathname, RULES_URL_PATH); this.dbUrl = parsedUrl.toString(); this.httpClient = new AuthorizedHttpClient(app); @@ -131,7 +179,7 @@ class DatabaseRulesClient { * Gets the currently applied security rules as a string. The return value consists of * the rules source including comments. * - * @return {Promise} A promise fulfilled with the rules as a raw string. + * @return A promise fulfilled with the rules as a raw string. */ public getRules(): Promise { const req: HttpRequestConfig = { @@ -160,7 +208,7 @@ class DatabaseRulesClient { const req: HttpRequestConfig = { method: 'GET', url: this.dbUrl, - data: {format: 'strict'}, + data: { format: 'strict' }, }; return this.httpClient.send(req) .then((resp) => { @@ -181,7 +229,7 @@ class DatabaseRulesClient { */ public setRules(source: string | Buffer | object): Promise { if (!validator.isNonEmptyString(source) && - !validator.isBuffer(source) && + !validator.isBuffer(source) && !validator.isNonNullObject(source)) { const error = new FirebaseDatabaseError({ code: 'invalid-argument', @@ -220,7 +268,7 @@ class DatabaseRulesClient { private getErrorMessage(err: HttpError): string { const intro = 'Error while accessing security rules'; try { - const body: {error?: string} = err.response.data; + const body: { error?: string } = err.response.data; if (body && body.error) { return `${intro}: ${body.error.trim()}`; } @@ -231,3 +279,14 @@ class DatabaseRulesClient { return `${intro}: ${err.response.text}`; } } + +function extractNamespace(parsedUrl: URL): string { + const ns = parsedUrl.searchParams.get('ns'); + if (ns) { + return ns; + } + + const hostname = parsedUrl.hostname; + const dotIndex = hostname.indexOf('.'); + return hostname.substring(0, dotIndex).toLowerCase(); +} diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000000..dae13a3654 --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1,97 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; +import { ServerValue as sv } from '@firebase/database'; +import * as rtdb from '@firebase/database-types'; + +/** + * Gets the {@link database.Database `Database`} service for the default + * app or a given app. + * + * `admin.database()` can be called with no arguments to access the default + * app's {@link database.Database `Database`} service or as + * `admin.database(app)` to access the + * {@link database.Database `Database`} service associated with a specific + * app. + * + * `admin.database` is also a namespace that can be used to access global + * constants and methods associated with the `Database` service. + * + * @example + * ```javascript + * // Get the Database service for the default app + * var defaultDatabase = admin.database(); + * ``` + * + * @example + * ```javascript + * // Get the Database service for a specific app + * var otherDatabase = admin.database(app); + * ``` + * + * @param App whose `Database` service to + * return. If not provided, the default `Database` service will be returned. + * + * @return The default `Database` service if no app + * is provided or the `Database` service associated with the provided app. + */ +export declare function database(app?: app.App): database.Database; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace database { + export interface Database extends rtdb.FirebaseDatabase { + /** + * Gets the currently applied security rules as a string. The return value consists of + * the rules source including comments. + * + * @return A promise fulfilled with the rules as a raw string. + */ + getRules(): Promise; + + /** + * Gets the currently applied security rules as a parsed JSON object. Any comments in + * the original source are stripped away. + * + * @return A promise fulfilled with the parsed rules object. + */ + getRulesJSON(): Promise; + + /** + * Sets the specified rules on the Firebase Realtime Database instance. If the rules source is + * specified as a string or a Buffer, it may include comments. + * + * @param source Source of the rules to apply. Must not be `null` or empty. + * @return Resolves when the rules are set on the Realtime Database. + */ + setRules(source: string | Buffer | object): Promise; + } + + /* eslint-disable @typescript-eslint/no-unused-vars */ + export import DataSnapshot = rtdb.DataSnapshot; + export import EventType = rtdb.EventType; + export import OnDisconnect = rtdb.OnDisconnect; + export import Query = rtdb.Query; + export import Reference = rtdb.Reference; + export import ThenableReference = rtdb.ThenableReference; + export import enableLogging = rtdb.enableLogging; + + /** + * [`ServerValue`](https://firebase.google.com/docs/reference/js/firebase.database.ServerValue) + * module from the `@firebase/database` package. + */ + export const ServerValue: rtdb.ServerValue = sv; +} diff --git a/src/default-namespace.ts b/src/default-namespace.ts index e82e6b6b7a..d15f3cae02 100644 --- a/src/default-namespace.ts +++ b/src/default-namespace.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,7 +15,7 @@ * limitations under the License. */ -import {FirebaseNamespace} from './firebase-namespace'; +import { FirebaseNamespace } from './firebase-namespace'; const firebaseAdmin = new FirebaseNamespace(); diff --git a/src/firebase-app.ts b/src/firebase-app.ts index 1f01d61986..29afbb75d2 100644 --- a/src/firebase-app.ts +++ b/src/firebase-app.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,46 +15,38 @@ * limitations under the License. */ -import {Credential, GoogleOAuthAccessToken, getApplicationDefault} from './auth/credential'; +import { AppOptions, app } from './firebase-namespace-api'; +import { credential } from './credential/index'; +import { getApplicationDefault } from './credential/credential-internal'; import * as validator from './utils/validator'; -import {deepCopy, deepExtend} from './utils/deep-copy'; -import {FirebaseServiceInterface} from './firebase-service'; -import {FirebaseNamespaceInternals} from './firebase-namespace'; -import {AppErrorCodes, FirebaseAppError} from './utils/error'; - -import {Auth} from './auth/auth'; -import {MachineLearning} from './machine-learning/machine-learning'; -import {Messaging} from './messaging/messaging'; -import {Storage} from './storage/storage'; -import {Database} from '@firebase/database'; -import {DatabaseService} from './database/database'; -import {Firestore} from '@google-cloud/firestore'; -import {FirestoreService} from './firestore/firestore'; -import {InstanceId} from './instance-id/instance-id'; - -import {ProjectManagement} from './project-management/project-management'; -import {SecurityRules} from './security-rules/security-rules'; +import { deepCopy } from './utils/deep-copy'; +import { FirebaseNamespaceInternals } from './firebase-namespace'; +import { AppErrorCodes, FirebaseAppError } from './utils/error'; + +import { Auth } from './auth/auth'; +import { MachineLearning } from './machine-learning/machine-learning'; +import { Messaging } from './messaging/messaging'; +import { Storage } from './storage/storage'; +import { database } from './database/index'; +import { DatabaseService } from './database/database-internal'; +import { Firestore } from '@google-cloud/firestore'; +import { FirestoreService } from './firestore/firestore-internal'; +import { Installations } from './installations/installations'; +import { InstanceId } from './instance-id/instance-id'; +import { ProjectManagement } from './project-management/project-management'; +import { SecurityRules } from './security-rules/security-rules'; import { RemoteConfig } from './remote-config/remote-config'; +import { AppCheck } from './app-check/app-check'; -import {Agent} from 'http'; +import Credential = credential.Credential; +import Database = database.Database; -/** - * Type representing a callback which is called every time an app lifecycle event occurs. - */ -export type AppHook = (event: string, app: FirebaseApp) => void; +const TOKEN_EXPIRY_THRESHOLD_MILLIS = 5 * 60 * 1000; /** - * Type representing the options object passed into initializeApp(). + * Type representing a callback which is called every time an app lifecycle event occurs. */ -export interface FirebaseAppOptions { - credential?: Credential; - databaseAuthVariableOverride?: object; - databaseURL?: string; - serviceAccountId?: string; - storageBucket?: string; - projectId?: string; - httpAgent?: Agent; -} +export type AppHook = (event: string, app: app.App) => void; /** * Type representing a Firebase OAuth access token (derived from a Google OAuth2 access token) which @@ -68,129 +61,86 @@ export interface FirebaseAccessToken { * Internals of a FirebaseApp instance. */ export class FirebaseAppInternals { - private isDeleted_ = false; private cachedToken_: FirebaseAccessToken; - private cachedTokenPromise_: Promise | null; private tokenListeners_: Array<(token: string) => void>; - private tokenRefreshTimeout_: NodeJS.Timer; constructor(private credential_: Credential) { this.tokenListeners_ = []; } - /** - * Gets an auth token for the associated app. - * - * @param {boolean} forceRefresh Whether or not to force a token refresh. - * @return {Promise} A Promise that will be fulfilled with the current or - * new token. - */ - public getToken(forceRefresh?: boolean): Promise { - const expired = this.cachedToken_ && this.cachedToken_.expirationTime < Date.now(); - if (this.cachedTokenPromise_ && !forceRefresh && !expired) { - return this.cachedTokenPromise_ - .catch((error) => { - // Update the cached token promise to avoid caching errors. Set it to resolve with the - // cached token if we have one (and return that promise since the token has still not - // expired). - if (this.cachedToken_) { - this.cachedTokenPromise_ = Promise.resolve(this.cachedToken_); - return this.cachedTokenPromise_; - } - - // Otherwise, set the cached token promise to null so that it will force a refresh next - // time getToken() is called. - this.cachedTokenPromise_ = null; - - // And re-throw the caught error. - throw error; - }); - } else { - // Clear the outstanding token refresh timeout. This is a noop if the timeout is undefined. - clearTimeout(this.tokenRefreshTimeout_); - - // this.credential_ may be an external class; resolving it in a promise helps us - // protect against exceptions and upgrades the result to a promise in all cases. - this.cachedTokenPromise_ = Promise.resolve(this.credential_.getAccessToken()) - .then((result: GoogleOAuthAccessToken) => { - // Since the developer can provide the credential implementation, we want to weakly verify - // the return type until the type is properly exported. - if (!validator.isNonNullObject(result) || - typeof result.expires_in !== 'number' || - typeof result.access_token !== 'string') { - throw new FirebaseAppError( - AppErrorCodes.INVALID_CREDENTIAL, - `Invalid access token generated: "${JSON.stringify(result)}". Valid access ` + - 'tokens must be an object with the "expires_in" (number) and "access_token" ' + - '(string) properties.', - ); - } - - const token: FirebaseAccessToken = { - accessToken: result.access_token, - expirationTime: Date.now() + (result.expires_in * 1000), - }; - - const hasAccessTokenChanged = (this.cachedToken_ && this.cachedToken_.accessToken !== token.accessToken); - const hasExpirationChanged = (this.cachedToken_ && this.cachedToken_.expirationTime !== token.expirationTime); - if (!this.cachedToken_ || hasAccessTokenChanged || hasExpirationChanged) { - this.cachedToken_ = token; - this.tokenListeners_.forEach((listener) => { - listener(token.accessToken); - }); - } - - // Establish a timeout to proactively refresh the token every minute starting at five - // minutes before it expires. Once a token refresh succeeds, no further retries are - // needed; if it fails, retry every minute until the token expires (resulting in a total - // of four retries: at 4, 3, 2, and 1 minutes). - let refreshTimeInSeconds = (result.expires_in - (5 * 60)); - let numRetries = 4; - - // In the rare cases the token is short-lived (that is, it expires in less than five - // minutes from when it was fetched), establish the timeout to refresh it after the - // current minute ends and update the number of retries that should be attempted before - // the token expires. - if (refreshTimeInSeconds <= 0) { - refreshTimeInSeconds = result.expires_in % 60; - numRetries = Math.floor(result.expires_in / 60) - 1; - } - - // The token refresh timeout keeps the Node.js process alive, so only create it if this - // instance has not already been deleted. - if (numRetries && !this.isDeleted_) { - this.setTokenRefreshTimeout(refreshTimeInSeconds * 1000, numRetries); - } - - return token; - }) - .catch((error) => { - let errorMessage = (typeof error === 'string') ? error : error.message; - - errorMessage = 'Credential implementation provided to initializeApp() via the ' + - '"credential" property failed to fetch a valid Google OAuth2 access token with the ' + - `following error: "${errorMessage}".`; - - if (errorMessage.indexOf('invalid_grant') !== -1) { - errorMessage += ' There are two likely causes: (1) your server time is not properly ' + - 'synced or (2) your certificate key file has been revoked. To solve (1), re-sync the ' + - 'time on your server. To solve (2), make sure the key ID for your key file is still ' + - 'present at https://console.firebase.google.com/iam-admin/serviceaccounts/project. If ' + - 'not, generate a new key file at ' + - 'https://console.firebase.google.com/project/_/settings/serviceaccounts/adminsdk.'; - } - - throw new FirebaseAppError(AppErrorCodes.INVALID_CREDENTIAL, errorMessage); - }); - - return this.cachedTokenPromise_; + public getToken(forceRefresh = false): Promise { + if (forceRefresh || this.shouldRefresh()) { + return this.refreshToken(); } + + return Promise.resolve(this.cachedToken_); + } + + 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 {function(string)} listener The listener that will be called with each new token. + * @param listener The listener that will be called with each new token. */ public addAuthTokenListener(listener: (token: string) => void): void { this.tokenListeners_.push(listener); @@ -202,65 +152,32 @@ export class FirebaseAppInternals { /** * Removes a token listener. * - * @param {function(string)} listener The listener to remove. + * @param listener The listener to remove. */ public removeAuthTokenListener(listener: (token: string) => void): void { this.tokenListeners_ = this.tokenListeners_.filter((other) => other !== listener); } - - /** - * Deletes the FirebaseAppInternals instance. - */ - public delete(): void { - this.isDeleted_ = true; - - // Clear the token refresh timeout so it doesn't keep the Node.js process alive. - clearTimeout(this.tokenRefreshTimeout_); - } - - /** - * Establishes timeout to refresh the Google OAuth2 access token used by the SDK. - * - * @param {number} delayInMilliseconds The delay to use for the timeout. - * @param {number} numRetries The number of times to retry fetching a new token if the prior fetch - * failed. - */ - private setTokenRefreshTimeout(delayInMilliseconds: number, numRetries: number): void { - this.tokenRefreshTimeout_ = setTimeout(() => { - this.getToken(/* forceRefresh */ true) - .catch(() => { - // Ignore the error since this might just be an intermittent failure. If we really cannot - // refresh the token, an error will be logged once the existing token expires and we try - // to fetch a fresh one. - if (numRetries > 0) { - this.setTokenRefreshTimeout(60 * 1000, numRetries - 1); - } - }); - }, delayInMilliseconds); - } } - - /** * Global context object for a collection of services using a shared authentication state. */ -export class FirebaseApp { +export class FirebaseApp implements app.App { public INTERNAL: FirebaseAppInternals; private name_: string; - private options_: FirebaseAppOptions; - private services_: {[name: string]: FirebaseServiceInterface} = {}; + private options_: AppOptions; + private services_: {[name: string]: unknown} = {}; private isDeleted_ = false; - constructor(options: FirebaseAppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { + constructor(options: AppOptions, name: string, private firebaseInternals_: FirebaseNamespaceInternals) { this.name_ = name; - this.options_ = deepCopy(options) as FirebaseAppOptions; + this.options_ = deepCopy(options); if (!validator.isNonNullObject(this.options_)) { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_OPTIONS, - `Invalid Firebase app options passed as the first argument to initializeApp() for the ` + + 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + `app named "${this.name_}". Options must be a non-null object.`, ); } @@ -274,24 +191,19 @@ export class FirebaseApp { if (typeof credential !== 'object' || credential === null || typeof credential.getAccessToken !== 'function') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_OPTIONS, - `Invalid Firebase app options passed as the first argument to initializeApp() for the ` + + 'Invalid Firebase app options passed as the first argument to initializeApp() for the ' + `app named "${this.name_}". The "credential" property must be an object which implements ` + - `the Credential interface.`, + 'the Credential interface.', ); } - Object.keys(firebaseInternals_.serviceFactories).forEach((serviceName) => { - // Defer calling createService() until the service is accessed - (this as {[key: string]: any})[serviceName] = this.getService_.bind(this, serviceName); - }); - this.INTERNAL = new FirebaseAppInternals(credential); } /** * Returns the Auth service instance associated with this app. * - * @return {Auth} The Auth service instance of this app. + * @return The Auth service instance of this app. */ public auth(): Auth { return this.ensureService_('auth', () => { @@ -303,11 +215,11 @@ export class FirebaseApp { /** * Returns the Database service for the specified URL, and the current app. * - * @return {Database} The Database service instance of this app. + * @return The Database service instance of this app. */ public database(url?: string): Database { const service: DatabaseService = this.ensureService_('database', () => { - const dbService: typeof DatabaseService = require('./database/database').DatabaseService; + const dbService: typeof DatabaseService = require('./database/database-internal').DatabaseService; return new dbService(this); }); return service.getDatabase(url); @@ -316,7 +228,7 @@ export class FirebaseApp { /** * Returns the Messaging service instance associated with this app. * - * @return {Messaging} The Messaging service instance of this app. + * @return The Messaging service instance of this app. */ public messaging(): Messaging { return this.ensureService_('messaging', () => { @@ -328,7 +240,7 @@ export class FirebaseApp { /** * Returns the Storage service instance associated with this app. * - * @return {Storage} The Storage service instance of this app. + * @return The Storage service instance of this app. */ public storage(): Storage { return this.ensureService_('storage', () => { @@ -339,16 +251,30 @@ export class FirebaseApp { public firestore(): Firestore { const service: FirestoreService = this.ensureService_('firestore', () => { - const firestoreService: typeof FirestoreService = require('./firestore/firestore').FirestoreService; + const firestoreService: typeof FirestoreService = require('./firestore/firestore-internal').FirestoreService; return new firestoreService(this); }); return service.client; } + /** + * Returns the `Installations` service instance associated with this app. + * + * @return The `Installations` service instance of this app. + */ + public installations(): Installations { + return this.ensureService_('installations', () => { + const fisService: typeof Installations = require('./installations/installations').Installations; + return new fisService(this); + }); + } + /** * Returns the InstanceId service instance associated with this app. * - * @return {InstanceId} The InstanceId service instance of this app. + * This API is deprecated. Use the `installations()` API instead. + * + * @return The InstanceId service instance of this app. */ public instanceId(): InstanceId { return this.ensureService_('iid', () => { @@ -360,7 +286,7 @@ export class FirebaseApp { /** * Returns the MachineLearning service instance associated with this app. * - * @return {MachineLearning} The Machine Learning service instance of this app + * @return The Machine Learning service instance of this app */ public machineLearning(): MachineLearning { return this.ensureService_('machine-learning', () => { @@ -373,7 +299,7 @@ export class FirebaseApp { /** * Returns the ProjectManagement service instance associated with this app. * - * @return {ProjectManagement} The ProjectManagement service instance of this app. + * @return The ProjectManagement service instance of this app. */ public projectManagement(): ProjectManagement { return this.ensureService_('project-management', () => { @@ -386,7 +312,7 @@ export class FirebaseApp { /** * Returns the SecurityRules service instance associated with this app. * - * @return {SecurityRules} The SecurityRules service instance of this app. + * @return The SecurityRules service instance of this app. */ public securityRules(): SecurityRules { return this.ensureService_('security-rules', () => { @@ -399,7 +325,7 @@ export class FirebaseApp { /** * Returns the RemoteConfig service instance associated with this app. * - * @return {RemoteConfig} The RemoteConfig service instance of this app. + * @return The RemoteConfig service instance of this app. */ public remoteConfig(): RemoteConfig { return this.ensureService_('remoteConfig', () => { @@ -408,10 +334,22 @@ export class FirebaseApp { }); } + /** + * Returns the AppCheck service instance associated with this app. + * + * @return The AppCheck service instance of this app. + */ + public appCheck(): AppCheck { + return this.ensureService_('appCheck', () => { + const appCheckService: typeof AppCheck = require('./app-check/app-check').AppCheck; + return new appCheckService(this); + }); + } + /** * Returns the name of the FirebaseApp instance. * - * @return {string} The name of the FirebaseApp instance. + * @return The name of the FirebaseApp instance. */ get name(): string { this.checkDestroyed_(); @@ -421,70 +359,41 @@ export class FirebaseApp { /** * Returns the options for the FirebaseApp instance. * - * @return {FirebaseAppOptions} The options for the FirebaseApp instance. + * @return The options for the FirebaseApp instance. */ - get options(): FirebaseAppOptions { + get options(): AppOptions { this.checkDestroyed_(); - return deepCopy(this.options_) as FirebaseAppOptions; + return deepCopy(this.options_); } /** * Deletes the FirebaseApp instance. * - * @return {Promise} An empty Promise fulfilled once the FirebaseApp instance is deleted. + * @return An empty Promise fulfilled once the FirebaseApp instance is deleted. */ public delete(): Promise { this.checkDestroyed_(); this.firebaseInternals_.removeApp(this.name_); - this.INTERNAL.delete(); - return Promise.all(Object.keys(this.services_).map((serviceName) => { - return this.services_[serviceName].INTERNAL.delete(); + const service = this.services_[serviceName]; + if (isStateful(service)) { + return service.delete(); + } + return Promise.resolve(); })).then(() => { this.services_ = {}; this.isDeleted_ = true; }); } - private ensureService_(serviceName: string, initializer: () => T): T { + 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), - ); + this.services_[serviceName] = initializer(); } - return this.services_[serviceName]; - } - - /** - * Callback function used to extend an App instance at the time of service instance creation. - */ - private extendApp_(props: {[prop: string]: any}): void { - deepExtend(this, props); + return this.services_[serviceName] as T; } /** @@ -499,3 +408,11 @@ export class FirebaseApp { } } } + +interface StatefulFirebaseService { + delete(): Promise; +} + +function isStateful(service: any): service is StatefulFirebaseService { + return typeof service.delete === 'function'; +} diff --git a/src/firebase-namespace-api.ts b/src/firebase-namespace-api.ts new file mode 100644 index 0000000000..509a2d1d18 --- /dev/null +++ b/src/firebase-namespace-api.ts @@ -0,0 +1,271 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Agent } from 'http'; +import { appCheck } from './app-check/index'; +import { auth } from './auth/index'; +import { credential } from './credential/index'; +import { database } from './database/index'; +import { firestore } from './firestore/index'; +import { installations } from './installations/index'; +import { instanceId } from './instance-id/index'; +import { machineLearning } from './machine-learning/index'; +import { messaging } from './messaging/index'; +import { projectManagement } from './project-management/index'; +import { remoteConfig } from './remote-config/index'; +import { securityRules } from './security-rules/index'; +import { storage } from './storage/index'; + +/** + * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In + * addition to a message string and stack trace, it contains a string code. + */ +export interface FirebaseError { + + /** + * Error codes are strings using the following format: `"service/string-code"`. + * Some examples include `"auth/invalid-uid"` and + * `"messaging/invalid-recipient"`. + * + * While the message for a given error can change, the code will remain the same + * between backward-compatible versions of the Firebase SDK. + */ + code: string; + + /** + * An explanatory message for the error that just occurred. + * + * This message is designed to be helpful to you, the developer. Because + * it generally does not convey meaningful information to end users, + * this message should not be displayed in your application. + */ + message: string; + + /** + * A string value containing the execution backtrace when the error originally + * occurred. + * + * This information can be useful to you and can be sent to + * {@link https://firebase.google.com/support/ Firebase Support} to help + * explain the cause of an error. + */ + stack?: string; + + /** + * @return A JSON-serializable representation of this object. + */ + toJSON(): object; +} + +/** + * Composite type which includes both a `FirebaseError` object and an index + * which can be used to get the errored item. + * + * @example + * ```javascript + * var registrationTokens = [token1, token2, token3]; + * admin.messaging().subscribeToTopic(registrationTokens, 'topic-name') + * .then(function(response) { + * if (response.failureCount > 0) { + * console.log("Following devices unsucessfully subscribed to topic:"); + * response.errors.forEach(function(error) { + * var invalidToken = registrationTokens[error.index]; + * console.log(invalidToken, error.error); + * }); + * } else { + * console.log("All devices successfully subscribed to topic:", response); + * } + * }) + * .catch(function(error) { + * console.log("Error subscribing to topic:", error); + * }); + *``` + */ +export interface FirebaseArrayIndexError { + + /** + * The index of the errored item within the original array passed as part of the + * called API method. + */ + index: number; + + /** + * The error object. + */ + error: FirebaseError; +} + +/** + * Available options to pass to [`initializeApp()`](admin#.initializeApp). + */ +export interface AppOptions { + + /** + * A {@link credential.Credential `Credential`} object used to + * authenticate the Admin SDK. + * + * See [Initialize the SDK](/docs/admin/setup#initialize_the_sdk) for detailed + * documentation and code samples. + */ + credential?: credential.Credential; + + /** + * The object to use as the [`auth`](/docs/reference/security/database/#auth) + * variable in your Realtime Database Rules when the Admin SDK reads from or + * writes to the Realtime Database. This allows you to downscope the Admin SDK + * from its default full read and write privileges. + * + * You can pass `null` to act as an unauthenticated client. + * + * See + * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) + * for detailed documentation and code samples. + */ + databaseAuthVariableOverride?: object | null; + + /** + * The URL of the Realtime Database from which to read and write data. + */ + databaseURL?: string; + + /** + * The ID of the service account to be used for signing custom tokens. This + * can be found in the `client_email` field of a service account JSON file. + */ + serviceAccountId?: string; + + /** + * The name of the Google Cloud Storage bucket used for storing application data. + * Use only the bucket name without any prefixes or additions (do *not* prefix + * the name with "gs://"). + */ + storageBucket?: string; + + /** + * The ID of the Google Cloud project associated with the App. + */ + projectId?: string; + + /** + * An [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) + * to be used when making outgoing HTTP calls. This Agent instance is used + * by all services that make REST calls (e.g. `auth`, `messaging`, + * `projectManagement`). + * + * Realtime Database and Firestore use other means of communicating with + * the backend servers, so they do not use this HTTP Agent. `Credential` + * instances also do not use this HTTP Agent, but instead support + * specifying an HTTP Agent in the corresponding factory methods. + */ + httpAgent?: Agent; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace app { + /** + * A Firebase app holds the initialization information for a collection of + * services. + * + * Do not call this constructor directly. Instead, use + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`} + * to create an app. + */ + export interface App { + + /** + * The (read-only) name for this app. + * + * The default app's name is `"[DEFAULT]"`. + * + * @example + * ```javascript + * // The default app's name is "[DEFAULT]" + * admin.initializeApp(defaultAppConfig); + * console.log(admin.app().name); // "[DEFAULT]" + * ``` + * + * @example + * ```javascript + * // A named app's name is what you provide to initializeApp() + * var otherApp = admin.initializeApp(otherAppConfig, "other"); + * console.log(otherApp.name); // "other" + * ``` + */ + name: string; + + /** + * The (read-only) configuration options for this app. These are the original + * parameters given in + * {@link + * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp + * `admin.initializeApp()`}. + * + * @example + * ```javascript + * var app = admin.initializeApp(config); + * console.log(app.options.credential === config.credential); // true + * console.log(app.options.databaseURL === config.databaseURL); // true + * ``` + */ + options: AppOptions; + + appCheck(): appCheck.AppCheck; + auth(): auth.Auth; + database(url?: string): database.Database; + firestore(): firestore.Firestore; + installations(): installations.Installations; + /** @deprecated */ + instanceId(): instanceId.InstanceId; + machineLearning(): machineLearning.MachineLearning; + messaging(): messaging.Messaging; + projectManagement(): projectManagement.ProjectManagement; + remoteConfig(): remoteConfig.RemoteConfig; + securityRules(): securityRules.SecurityRules; + storage(): storage.Storage; + + /** + * Renders this local `FirebaseApp` unusable and frees the resources of + * all associated services (though it does *not* clean up any backend + * resources). When running the SDK locally, this method + * must be called to ensure graceful termination of the process. + * + * @example + * ```javascript + * app.delete() + * .then(function() { + * console.log("App deleted successfully"); + * }) + * .catch(function(error) { + * console.log("Error deleting app:", error); + * }); + * ``` + */ + delete(): Promise; + } +} + +// Declare other top-level members of the admin namespace below. Unfortunately, there's no +// compile-time mechanism to ensure that the FirebaseNamespace class actually provides these +// signatures. But this part of the API is quite small and stable. It should be easy enough to +// enforce conformance via disciplined coding and good integration tests. + +export declare const SDK_VERSION: string; +export declare const apps: (app.App | null)[]; + +export declare function app(name?: string): app.App; +export declare function initializeApp(options?: AppOptions, name?: string): app.App; diff --git a/src/project-management/app-metadata.ts b/src/firebase-namespace.d.ts similarity index 50% rename from src/project-management/app-metadata.ts rename to src/firebase-namespace.d.ts index 7f55551d53..bff64ccdf3 100644 --- a/src/project-management/app-metadata.ts +++ b/src/firebase-namespace.d.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019 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,26 +14,17 @@ * limitations under the License. */ -export enum AppPlatform { - PLATFORM_UNKNOWN = 'PLATFORM_UNKNOWN', - IOS = 'IOS', - ANDROID = 'ANDROID', -} - -export interface AppMetadata { - appId: string; - displayName?: string; - platform: AppPlatform; - projectId: string; - resourceName: string; -} - -export interface AndroidAppMetadata extends AppMetadata { - platform: AppPlatform.ANDROID; - packageName: string; -} - -export interface IosAppMetadata extends AppMetadata { - platform: AppPlatform.IOS; - bundleId: string; -} +export * from './credential/index'; +export * from './firebase-namespace-api'; +export * from './app-check/index'; +export * from './auth/index'; +export * from './database/index'; +export * from './firestore/index'; +export * from './installations/index'; +export * from './instance-id/index'; +export * from './machine-learning/index'; +export * from './messaging/index'; +export * from './project-management/index'; +export * from './remote-config/index'; +export * from './security-rules/index'; +export * from './storage/index'; diff --git a/src/firebase-namespace.ts b/src/firebase-namespace.ts index 2247387d18..311807301a 100644 --- a/src/firebase-namespace.ts +++ b/src/firebase-namespace.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,30 +16,42 @@ */ import fs = require('fs'); -import {Agent} from 'http'; -import {deepExtend} from './utils/deep-copy'; -import {AppErrorCodes, FirebaseAppError} from './utils/error'; -import {AppHook, FirebaseApp, FirebaseAppOptions} from './firebase-app'; -import {FirebaseServiceFactory, FirebaseServiceInterface} from './firebase-service'; -import { - Credential, - RefreshTokenCredential, - ServiceAccountCredential, - getApplicationDefault, -} from './auth/credential'; - -import {Auth} from './auth/auth'; -import {MachineLearning} from './machine-learning/machine-learning'; -import {Messaging} from './messaging/messaging'; -import {Storage} from './storage/storage'; -import {Database} from '@firebase/database'; -import {Firestore} from '@google-cloud/firestore'; -import {InstanceId} from './instance-id/instance-id'; -import {ProjectManagement} from './project-management/project-management'; -import { SecurityRules } from './security-rules/security-rules'; -import { RemoteConfig } from './remote-config/remote-config'; + +import { AppErrorCodes, FirebaseAppError } from './utils/error'; +import { AppOptions, app } from './firebase-namespace-api'; +import { FirebaseApp } from './firebase-app'; +import { cert, refreshToken, applicationDefault } from './credential/credential'; +import { getApplicationDefault } from './credential/credential-internal'; + +import { appCheck } from './app-check/index'; +import { auth } from './auth/index'; +import { database } from './database/index'; +import { firestore } from './firestore/index'; +import { installations } from './installations/index'; +import { instanceId } from './instance-id/index'; +import { machineLearning } from './machine-learning/index'; +import { messaging } from './messaging/index'; +import { projectManagement } from './project-management/index'; +import { remoteConfig } from './remote-config/index'; +import { securityRules } from './security-rules/index'; +import { storage } from './storage/index'; import * as validator from './utils/validator'; +import { getSdkVersion } from './utils/index'; + +import App = app.App; +import 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; const DEFAULT_APP_NAME = '[DEFAULT]'; @@ -49,42 +62,31 @@ const DEFAULT_APP_NAME = '[DEFAULT]'; */ export const FIREBASE_CONFIG_VAR = 'FIREBASE_CONFIG'; - -let globalAppDefaultCred: Credential; -const globalCertCreds: { [key: string]: ServiceAccountCredential } = {}; -const globalRefreshTokenCreds: { [key: string]: RefreshTokenCredential } = {}; - - export interface FirebaseServiceNamespace { - (app?: FirebaseApp): T; + (app?: App): T; [key: string]: any; } - /** * Internals of a FirebaseNamespace instance. */ export class FirebaseNamespaceInternals { - public serviceFactories: {[serviceName: string]: FirebaseServiceFactory} = {}; - - private apps_: {[appName: string]: FirebaseApp} = {}; - private appHooks_: {[service: string]: AppHook} = {}; + private apps_: {[appName: string]: App} = {}; constructor(public firebase_: {[key: string]: any}) {} /** - * Initializes the FirebaseApp instance. + * Initializes the App instance. * - * @param {FirebaseAppOptions} options Optional options for the FirebaseApp instance. If none present - * will try to initialize from the FIREBASE_CONFIG environment variable. - * If the environment variable contains a string that starts with '{' - * it will be parsed as JSON, - * otherwise it will be assumed to be pointing to a file. - * @param {string} [appName] Optional name of the FirebaseApp instance. + * @param options Optional options for the App instance. If none present will try to initialize + * from the FIREBASE_CONFIG environment variable. If the environment variable contains a string + * that starts with '{' it will be parsed as JSON, otherwise it will be assumed to be pointing + * to a file. + * @param appName Optional name of the FirebaseApp instance. * - * @return {FirebaseApp} A new FirebaseApp instance. + * @return A new App instance. */ - public initializeApp(options?: FirebaseAppOptions, appName = DEFAULT_APP_NAME): FirebaseApp { + public initializeApp(options?: AppOptions, appName = DEFAULT_APP_NAME): App { if (typeof options === 'undefined') { options = this.loadOptionsFromEnvVar(); options.credential = getApplicationDefault(); @@ -115,22 +117,18 @@ export class FirebaseNamespaceInternals { } 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 + * Returns the App instance with the provided name (or the default App instance * if no name is provided). * - * @param {string} [appName=DEFAULT_APP_NAME] Optional name of the FirebaseApp instance to return. - * @return {FirebaseApp} The FirebaseApp instance which has the provided name. + * @param appName Optional name of the FirebaseApp instance to return. + * @return The App instance which has the provided name. */ - public app(appName = DEFAULT_APP_NAME): FirebaseApp { + public app(appName = DEFAULT_APP_NAME): App { if (typeof appName !== 'string' || appName === '') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_NAME, @@ -148,103 +146,26 @@ export class FirebaseNamespaceInternals { } /* - * Returns an array of all the non-deleted FirebaseApp instances. - * - * @return {Array} An array of all the non-deleted FirebaseApp instances + * Returns an array of all the non-deleted App instances. */ - public get apps(): FirebaseApp[] { + public get apps(): App[] { // Return a copy so the caller cannot mutate the array return Object.keys(this.apps_).map((appName) => this.apps_[appName]); } /* - * Removes the specified FirebaseApp instance. - * - * @param {string} appName The name of the FirebaseApp instance to remove. + * Removes the specified App instance. */ public removeApp(appName: string): void { if (typeof appName === 'undefined') { throw new FirebaseAppError( AppErrorCodes.INVALID_APP_NAME, - `No Firebase app name provided. App name must be a non-empty string.`, + 'No Firebase app name provided. App name must be a non-empty string.', ); } 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; - } - - // The service namespace is an accessor function which takes a FirebaseApp instance - // or uses the default app if no FirebaseApp instance is provided - const serviceNamespace: FirebaseServiceNamespace = (appArg?: FirebaseApp) => { - 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): void { - Object.keys(this.serviceFactories).forEach((serviceName) => { - if (this.appHooks_[serviceName]) { - this.appHooks_[serviceName](eventName, app); - } - }); + delete this.apps_[appToRemove.name]; } /** @@ -253,14 +174,14 @@ export class FirebaseNamespaceInternals { * If the environment variable contains a string that starts with '{' it will be parsed as JSON, * otherwise it will be assumed to be pointing to a file. */ - private loadOptionsFromEnvVar(): FirebaseAppOptions { + private loadOptionsFromEnvVar(): AppOptions { const config = process.env[FIREBASE_CONFIG_VAR]; if (!validator.isNonEmptyString(config)) { return {}; } try { const contents = config.startsWith('{') ? config : fs.readFileSync(config, 'utf8'); - return JSON.parse(contents) as FirebaseAppOptions; + return JSON.parse(contents) as AppOptions; } catch (error) { // Throw a nicely formed error message if the file contents cannot be parsed throw new FirebaseAppError( @@ -271,34 +192,10 @@ export class FirebaseNamespaceInternals { } } - const firebaseCredential = { - cert: (serviceAccountPathOrObject: string | object, httpAgent?: Agent): Credential => { - const stringifiedServiceAccount = JSON.stringify(serviceAccountPathOrObject); - if (!(stringifiedServiceAccount in globalCertCreds)) { - globalCertCreds[stringifiedServiceAccount] = new ServiceAccountCredential(serviceAccountPathOrObject, httpAgent); - } - return globalCertCreds[stringifiedServiceAccount]; - }, - - refreshToken: (refreshTokenPathOrObject: string | object, httpAgent?: Agent): Credential => { - const stringifiedRefreshToken = JSON.stringify(refreshTokenPathOrObject); - if (!(stringifiedRefreshToken in globalRefreshTokenCreds)) { - globalRefreshTokenCreds[stringifiedRefreshToken] = new RefreshTokenCredential( - refreshTokenPathOrObject, httpAgent); - } - return globalRefreshTokenCreds[stringifiedRefreshToken]; - }, - - applicationDefault: (httpAgent?: Agent): Credential => { - if (typeof globalAppDefaultCred === 'undefined') { - globalAppDefaultCred = getApplicationDefault(httpAgent); - } - return globalAppDefaultCred; - }, + cert, refreshToken, applicationDefault }; - /** * Global Firebase context object. */ @@ -309,7 +206,7 @@ export class FirebaseNamespace { /* tslint:enable:variable-name */ public credential = firebaseCredential; - public SDK_VERSION = ''; + public SDK_VERSION = getSdkVersion(); public INTERNAL: FirebaseNamespaceInternals; /* tslint:disable */ @@ -327,11 +224,11 @@ export class FirebaseNamespace { * `Auth` service for the default app or an explicitly specified app. */ get auth(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).auth(); }; const auth = require('./auth/auth').Auth; - return Object.assign(fn, {Auth: auth}); + return Object.assign(fn, { Auth: auth }); } /** @@ -339,7 +236,7 @@ export class FirebaseNamespace { * `Database` service for the default app or an explicitly specified app. */ get database(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).database(); }; @@ -352,11 +249,11 @@ export class FirebaseNamespace { * `Messaging` service for the default app or an explicitly specified app. */ get messaging(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).messaging(); }; const messaging = require('./messaging/messaging').Messaging; - return Object.assign(fn, {Messaging: messaging}); + return Object.assign(fn, { Messaging: messaging }); } /** @@ -364,11 +261,11 @@ export class FirebaseNamespace { * `Storage` service for the default app or an explicitly specified app. */ get storage(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).storage(); }; const storage = require('./storage/storage').Storage; - return Object.assign(fn, {Storage: storage}); + return Object.assign(fn, { Storage: storage }); } /** @@ -376,7 +273,7 @@ export class FirebaseNamespace { * `Firestore` service for the default app or an explicitly specified app. */ get firestore(): FirebaseServiceNamespace { - let fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + let fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).firestore(); }; @@ -408,12 +305,24 @@ export class FirebaseNamespace { */ get machineLearning(): FirebaseServiceNamespace { const fn: FirebaseServiceNamespace = - (app?: FirebaseApp) => { + (app?: App) => { return this.ensureApp(app).machineLearning(); }; const machineLearning = require('./machine-learning/machine-learning').MachineLearning; - return Object.assign(fn, {MachineLearning: 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 }); } /** @@ -421,11 +330,11 @@ export class FirebaseNamespace { * `Instance` service for the default app or an explicitly specified app. */ get instanceId(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).instanceId(); }; const instanceId = require('./instance-id/instance-id').InstanceId; - return Object.assign(fn, {InstanceId: instanceId}); + return Object.assign(fn, { InstanceId: instanceId }); } /** @@ -433,11 +342,11 @@ export class FirebaseNamespace { * `ProjectManagement` service for the default app or an explicitly specified app. */ get projectManagement(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).projectManagement(); }; const projectManagement = require('./project-management/project-management').ProjectManagement; - return Object.assign(fn, {ProjectManagement: projectManagement}); + return Object.assign(fn, { ProjectManagement: projectManagement }); } /** @@ -445,11 +354,11 @@ export class FirebaseNamespace { * `SecurityRules` service for the default app or an explicitly specified app. */ get securityRules(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).securityRules(); }; const securityRules = require('./security-rules/security-rules').SecurityRules; - return Object.assign(fn, {SecurityRules: securityRules}); + return Object.assign(fn, { SecurityRules: securityRules }); } /** @@ -457,25 +366,39 @@ export class FirebaseNamespace { * `RemoteConfig` service for the default app or an explicitly specified app. */ get remoteConfig(): FirebaseServiceNamespace { - const fn: FirebaseServiceNamespace = (app?: FirebaseApp) => { + const fn: FirebaseServiceNamespace = (app?: App) => { return this.ensureApp(app).remoteConfig(); }; const remoteConfig = require('./remote-config/remote-config').RemoteConfig; return Object.assign(fn, { RemoteConfig: remoteConfig }); } + /** + * 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 {FirebaseAppOptions} [options] Optional options for the FirebaseApp instance. + * @param options Optional options for the FirebaseApp instance. * If none present will try to initialize from the FIREBASE_CONFIG environment variable. * If the environment variable contains a string that starts with '{' it will be parsed as JSON, * otherwise it will be assumed to be pointing to a file. - * @param {string} [appName] Optional name of the FirebaseApp instance. + * @param appName Optional name of the FirebaseApp instance. * - * @return {FirebaseApp} A new FirebaseApp instance. + * @return A new FirebaseApp instance. */ - public initializeApp(options?: FirebaseAppOptions, appName?: string): FirebaseApp { + public initializeApp(options?: AppOptions, appName?: string): App { return this.INTERNAL.initializeApp(options, appName); } @@ -483,23 +406,21 @@ export class FirebaseNamespace { * Returns the FirebaseApp instance with the provided name (or the default FirebaseApp instance * if no name is provided). * - * @param {string} [appName] Optional name of the FirebaseApp instance to return. - * @return {FirebaseApp} The FirebaseApp instance which has the provided name. + * @param appName Optional name of the FirebaseApp instance to return. + * @return The FirebaseApp instance which has the provided name. */ - public app(appName?: string): FirebaseApp { + public app(appName?: string): App { return this.INTERNAL.app(appName); } /* * Returns an array of all the non-deleted FirebaseApp instances. - * - * @return {Array} An array of all the non-deleted FirebaseApp instances */ - public get apps(): FirebaseApp[] { + public get apps(): App[] { return this.INTERNAL.apps; } - private ensureApp(app?: FirebaseApp): FirebaseApp { + private ensureApp(app?: App): App { if (typeof app === 'undefined') { app = this.app(); } diff --git a/src/firebase-service.ts b/src/firebase-service.ts 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.ts b/src/firestore/firestore-internal.ts similarity index 78% rename from src/firestore/firestore.ts rename to src/firestore/firestore-internal.ts index 89e7fe2771..2188ec223a 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore-internal.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,32 +15,15 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseFirestoreError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ServiceAccountCredential, isApplicationDefault} from '../auth/credential'; -import {Firestore, Settings} from '@google-cloud/firestore'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseFirestoreError } from '../utils/error'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; +import { Firestore, Settings } from '@google-cloud/firestore'; import * as validator from '../utils/validator'; import * as utils from '../utils/index'; -/** - * Internals of a Firestore instance. - */ -class FirestoreInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - -export class FirestoreService implements FirebaseServiceInterface { - public INTERNAL: FirestoreInternals = new FirestoreInternals(); +export class FirestoreService { private appInternal: FirebaseApp; private firestoreClient: Firestore; @@ -90,7 +74,7 @@ export function getFirestoreOptions(app: FirebaseApp): Settings { // Try to use the Google application default credentials. // If an explicit project ID is not available, let Firestore client discover one from the // environment. This prevents the users from having to set GOOGLE_CLOUD_PROJECT in GCP runtimes. - return validator.isNonEmptyString(projectId) ? {projectId, firebaseVersion} : {firebaseVersion}; + return validator.isNonEmptyString(projectId) ? { projectId, firebaseVersion } : { firebaseVersion }; } throw new FirebaseFirestoreError({ diff --git a/src/firestore/index.ts b/src/firestore/index.ts new file mode 100644 index 0000000000..4efe99e053 --- /dev/null +++ b/src/firestore/index.ts @@ -0,0 +1,57 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; +import * as _firestore from '@google-cloud/firestore'; + +export declare function firestore(app?: 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 BulkWriter = _firestore.BulkWriter; + export import BulkWriterOptions = _firestore.BulkWriterOptions; + export import CollectionGroup = _firestore.CollectionGroup; + export import CollectionReference = _firestore.CollectionReference; + export import DocumentChangeType = _firestore.DocumentChangeType; + export import DocumentData = _firestore.DocumentData; + export import DocumentReference = _firestore.DocumentReference; + export import DocumentSnapshot = _firestore.DocumentSnapshot; + export import FieldPath = _firestore.FieldPath; + export import FieldValue = _firestore.FieldValue; + export import Firestore = _firestore.Firestore; + export import FirestoreDataConverter = _firestore.FirestoreDataConverter; + export import GeoPoint = _firestore.GeoPoint; + export import GrpcStatus = _firestore.GrpcStatus; + export import Precondition = _firestore.Precondition; + export import Query = _firestore.Query; + export import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; + export import QueryPartition = _firestore.QueryPartition; + export import QuerySnapshot = _firestore.QuerySnapshot; + export import ReadOptions = _firestore.ReadOptions; + export import Settings = _firestore.Settings; + export import Timestamp = _firestore.Timestamp; + export import Transaction = _firestore.Transaction; + export import UpdateData = _firestore.UpdateData; + export import WriteBatch = _firestore.WriteBatch; + export import WriteResult = _firestore.WriteResult; + + export import setLogFunction = _firestore.setLogFunction; +} diff --git a/src/index.d.ts b/src/index.d.ts old mode 100755 new mode 100644 index 3df285bed9..1807b1e079 --- 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,1399 +15,7 @@ * limitations under the License. */ -import { Bucket } from '@google-cloud/storage'; -import * as _firestore from '@google-cloud/firestore'; -import { Agent } from 'http'; - -import * as _auth from './auth'; -import * as _database from './database'; -import * as _messaging from './messaging'; -import * as _instanceId from './instance-id'; -import * as _projectManagement from './project-management'; -import * as _securityRules from './security-rules'; - -/* eslint-disable @typescript-eslint/ban-types */ - -/** - * `admin` is a global namespace from which all Firebase Admin - * services are accessed. - */ -declare namespace admin { - - /** - * `FirebaseError` is a subclass of the standard JavaScript `Error` object. In - * addition to a message string and stack trace, it contains a string code. - */ - interface FirebaseError { - - /** - * Error codes are strings using the following format: `"service/string-code"`. - * Some examples include `"auth/invalid-uid"` and - * `"messaging/invalid-recipient"`. - * - * While the message for a given error can change, the code will remain the same - * between backward-compatible versions of the Firebase SDK. - */ - code: string; - - /** - * An explanatory message for the error that just occurred. - * - * This message is designed to be helpful to you, the developer. Because - * it generally does not convey meaningful information to end users, - * this message should not be displayed in your application. - */ - message: string; - - /** - * A string value containing the execution backtrace when the error originally - * occurred. - * - * This information can be useful to you and can be sent to - * {@link https://firebase.google.com/support/ Firebase Support} to help - * explain the cause of an error. - */ - stack: string; - - /** - * @return A JSON-serializable representation of this object. - */ - toJSON(): Object; - } - - /** - * Composite type which includes both a `FirebaseError` object and an index - * which can be used to get the errored item. - * - * @example - * ```javascript - * var registrationTokens = [token1, token2, token3]; - * admin.messaging().subscribeToTopic(registrationTokens, 'topic-name') - * .then(function(response) { - * if (response.failureCount > 0) { - * console.log("Following devices unsucessfully subscribed to topic:"); - * response.errors.forEach(function(error) { - * var invalidToken = registrationTokens[error.index]; - * console.log(invalidToken, error.error); - * }); - * } else { - * console.log("All devices successfully subscribed to topic:", response); - * } - * }) - * .catch(function(error) { - * console.log("Error subscribing to topic:", error); - * }); - *``` - */ - interface FirebaseArrayIndexError { - - /** - * The index of the errored item within the original array passed as part of the - * called API method. - */ - index: number; - - /** - * The error object. - */ - error: FirebaseError; - } - - interface ServiceAccount { - projectId?: string; - clientEmail?: string; - privateKey?: string; - } - - interface GoogleOAuthAccessToken { - access_token: string; - expires_in: number; - } - - /** - * Available options to pass to [`initializeApp()`](admin#.initializeApp). - */ - interface AppOptions { - - /** - * A {@link admin.credential.Credential `Credential`} object used to - * authenticate the Admin SDK. - * - * See [Initialize the SDK](/docs/admin/setup#initialize_the_sdk) for detailed - * documentation and code samples. - */ - credential?: admin.credential.Credential; - - /** - * The object to use as the [`auth`](/docs/reference/security/database/#auth) - * variable in your Realtime Database Rules when the Admin SDK reads from or - * writes to the Realtime Database. This allows you to downscope the Admin SDK - * from its default full read and write privileges. - * - * You can pass `null` to act as an unauthenticated client. - * - * See - * [Authenticate with limited privileges](/docs/database/admin/start#authenticate-with-limited-privileges) - * for detailed documentation and code samples. - */ - databaseAuthVariableOverride?: Object | null; - - /** - * The URL of the Realtime Database from which to read and write data. - */ - databaseURL?: string; - - /** - * The ID of the service account to be used for signing custom tokens. This - * can be found in the `client_email` field of a service account JSON file. - */ - serviceAccountId?: string; - - /** - * The name of the Google Cloud Storage bucket used for storing application data. - * Use only the bucket name without any prefixes or additions (do *not* prefix - * the name with "gs://"). - */ - storageBucket?: string; - - /** - * The ID of the Google Cloud project associated with the App. - */ - projectId?: string; - - /** - * An [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when making outgoing HTTP calls. This Agent instance is used - * by all services that make REST calls (e.g. `auth`, `messaging`, - * `projectManagement`). - * - * Realtime Database and Firestore use other means of communicating with - * the backend servers, so they do not use this HTTP Agent. `Credential` - * instances also do not use this HTTP Agent, but instead support - * specifying an HTTP Agent in the corresponding factory methods. - */ - httpAgent?: Agent; - } - - const SDK_VERSION: string; - const apps: (admin.app.App | null)[]; - - function app(name?: string): admin.app.App; - - /** - * Gets the {@link admin.auth.Auth `Auth`} service for the default app or a - * given app. - * - * `admin.auth()` can be called with no arguments to access the default app's - * {@link admin.auth.Auth `Auth`} service or as `admin.auth(app)` to access the - * {@link admin.auth.Auth `Auth`} service associated with a specific app. - * - * @example - * ```javascript - * // Get the Auth service for the default app - * var defaultAuth = admin.auth(); - * ``` - * - * @example - * ```javascript - * // Get the Auth service for a given app - * var otherAuth = admin.auth(otherApp); - * ``` - * - */ - function auth(app?: admin.app.App): admin.auth.Auth; - - /** - * Gets the {@link admin.database.Database `Database`} service for the default - * app or a given app. - * - * `admin.database()` can be called with no arguments to access the default - * app's {@link admin.database.Database `Database`} service or as - * `admin.database(app)` to access the - * {@link admin.database.Database `Database`} service associated with a specific - * app. - * - * `admin.database` is also a namespace that can be used to access global - * constants and methods associated with the `Database` service. - * - * @example - * ```javascript - * // Get the Database service for the default app - * var defaultDatabase = admin.database(); - * ``` - * - * @example - * ```javascript - * // Get the Database service for a specific app - * var otherDatabase = admin.database(app); - * ``` - * - * @param App whose `Database` service to - * return. If not provided, the default `Database` service will be returned. - * - * @return The default `Database` service if no app - * is provided or the `Database` service associated with the provided app. - */ - function database(app?: admin.app.App): admin.database.Database; - - /** - * Gets the {@link admin.messaging.Messaging `Messaging`} service for the - * default app or a given app. - * - * `admin.messaging()` can be called with no arguments to access the default - * app's {@link admin.messaging.Messaging `Messaging`} service or as - * `admin.messaging(app)` to access the - * {@link admin.messaging.Messaging `Messaging`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Messaging service for the default app - * var defaultMessaging = admin.messaging(); - * ``` - * - * @example - * ```javascript - * // Get the Messaging service for a given app - * var otherMessaging = admin.messaging(otherApp); - * ``` - * - * @param app Optional app whose `Messaging` service to - * return. If not provided, the default `Messaging` service will be returned. - * - * @return The default `Messaging` service if no - * app is provided or the `Messaging` service associated with the provided - * app. - */ - function messaging(app?: admin.app.App): admin.messaging.Messaging; - - /** - * Gets the {@link admin.storage.Storage `Storage`} service for the - * default app or a given app. - * - * `admin.storage()` can be called with no arguments to access the default - * app's {@link admin.storage.Storage `Storage`} service or as - * `admin.storage(app)` to access the - * {@link admin.storage.Storage `Storage`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Storage service for the default app - * var defaultStorage = admin.storage(); - * ``` - * - * @example - * ```javascript - * // Get the Storage service for a given app - * var otherStorage = admin.storage(otherApp); - * ``` - */ - function storage(app?: admin.app.App): admin.storage.Storage; - - /** - * - * @param app A Firebase App instance - * @returns A [Firestore](https://cloud.google.com/nodejs/docs/reference/firestore/latest/Firestore) - * instance as defined in the `@google-cloud/firestore` package. - */ - function firestore(app?: admin.app.App): admin.firestore.Firestore; - - /** - * Gets the {@link admin.instanceId.InstanceId `InstanceId`} service for the - * default app or a given app. - * - * `admin.instanceId()` can be called with no arguments to access the default - * app's {@link admin.instanceId.InstanceId `InstanceId`} service or as - * `admin.instanceId(app)` to access the - * {@link admin.instanceId.InstanceId `InstanceId`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the Instance ID service for the default app - * var defaultInstanceId = admin.instanceId(); - * ``` - * - * @example - * ```javascript - * // Get the Instance ID service for a given app - * var otherInstanceId = admin.instanceId(otherApp); - *``` - * - * @param app Optional app whose `InstanceId` service to - * return. If not provided, the default `InstanceId` service will be - * returned. - * - * @return The default `InstanceId` service if - * no app is provided or the `InstanceId` service associated with the - * provided app. - */ - function instanceId(app?: admin.app.App): admin.instanceId.InstanceId; - - /** - * Gets the {@link admin.projectManagement.ProjectManagement - * `ProjectManagement`} service for the default app or a given app. - * - * `admin.projectManagement()` can be called with no arguments to access the - * default app's {@link admin.projectManagement.ProjectManagement - * `ProjectManagement`} service, or as `admin.projectManagement(app)` to access - * the {@link admin.projectManagement.ProjectManagement `ProjectManagement`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the ProjectManagement service for the default app - * var defaultProjectManagement = admin.projectManagement(); - * ``` - * - * @example - * ```javascript - * // Get the ProjectManagement service for a given app - * var otherProjectManagement = admin.projectManagement(otherApp); - * ``` - * - * @param app Optional app whose `ProjectManagement` service - * to return. If not provided, the default `ProjectManagement` service will - * be returned. * - * @return The default `ProjectManagement` service if no app is provided or the - * `ProjectManagement` service associated with the provided app. - */ - function projectManagement(app?: admin.app.App): admin.projectManagement.ProjectManagement; - - /** - * Gets the {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service for the - * default app or a given app. - * - * `admin.remoteConfig()` can be called with no arguments to access the default - * app's {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service or as - * `admin.remoteConfig(app)` to access the - * {@link admin.remoteConfig.RemoteConfig `RemoteConfig`} service associated with a - * specific app. - * - * @example - * ```javascript - * // Get the `RemoteConfig` service for the default app - * var defaultRemoteConfig = admin.remoteConfig(); - * ``` - * - * @example - * ```javascript - * // Get the `RemoteConfig` service for a given app - * var otherRemoteConfig = admin.remoteConfig(otherApp); - * ``` - * - * @param app Optional app for which to return the `RemoteConfig` service. - * If not provided, the default `RemoteConfig` service is returned. - * - * @return The default `RemoteConfig` service if no - * app is provided, or the `RemoteConfig` service associated with the provided - * app. - */ - function remoteConfig(app?: admin.app.App): admin.remoteConfig.RemoteConfig; - - /** - * Gets the {@link admin.securityRules.SecurityRules - * `SecurityRules`} service for the default app or a given app. - * - * `admin.securityRules()` can be called with no arguments to access the - * default app's {@link admin.securityRules.SecurityRules - * `SecurityRules`} service, or as `admin.securityRules(app)` to access - * the {@link admin.securityRules.SecurityRules `SecurityRules`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the SecurityRules service for the default app - * var defaultSecurityRules = admin.securityRules(); - * ``` - * - * @example - * ```javascript - * // Get the SecurityRules service for a given app - * var otherSecurityRules = admin.securityRules(otherApp); - * ``` - * - * @param app Optional app to return the `SecurityRules` service - * for. If not provided, the default `SecurityRules` service - * is returned. - * @return The default `SecurityRules` service if no app is provided, or the - * `SecurityRules` service associated with the provided app. - */ - function securityRules(app?: admin.app.App): admin.securityRules.SecurityRules; - - /** - * Gets the {@link admin.machineLearning.MachineLearning `MachineLearning`} service for the - * default app or a given app. - * - * `admin.machineLearning()` can be called with no arguments to access the - * default app's {@link admin.machineLearning.MachineLearning - * `MachineLearning`} service or as `admin.machineLearning(app)` to access - * the {@link admin.machineLearning.MachineLearning `MachineLearning`} - * service associated with a specific app. - * - * @example - * ```javascript - * // Get the MachineLearning service for the default app - * var defaultMachineLearning = admin.machineLearning(); - * ``` - * - * @example - * ```javascript - * // Get the MachineLearning service for a given app - * var otherMachineLearning = admin.machineLearning(otherApp); - * ``` - * - * @param app Optional app whose `MachineLearning` service to - * return. If not provided, the default `MachineLearning` service - * will be returned. - * - * @return The default `MachineLearning` service if no app is provided or the - * `MachineLearning` service associated with the provided app. - */ - function machineLearning(app?: admin.app.App): admin.machineLearning.MachineLearning; - - function initializeApp(options?: admin.AppOptions, name?: string): admin.app.App; -} - -declare namespace admin.app { - /** - * A Firebase app holds the initialization information for a collection of - * services. - * - * Do not call this constructor directly. Instead, use - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`} - * to create an app. - */ - interface App { - - /** - * The (read-only) name for this app. - * - * The default app's name is `"[DEFAULT]"`. - * - * @example - * ```javascript - * // The default app's name is "[DEFAULT]" - * admin.initializeApp(defaultAppConfig); - * console.log(admin.app().name); // "[DEFAULT]" - * ``` - * - * @example - * ```javascript - * // A named app's name is what you provide to initializeApp() - * var otherApp = admin.initializeApp(otherAppConfig, "other"); - * console.log(otherApp.name); // "other" - * ``` - */ - name: string; - - /** - * The (read-only) configuration options for this app. These are the original - * parameters given in - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * @example - * ```javascript - * var app = admin.initializeApp(config); - * console.log(app.options.credential === config.credential); // true - * console.log(app.options.databaseURL === config.databaseURL); // true - * ``` - */ - options: admin.AppOptions; - - - auth(): admin.auth.Auth; - database(url?: string): admin.database.Database; - firestore(): admin.firestore.Firestore; - instanceId(): admin.instanceId.InstanceId; - machineLearning(): admin.machineLearning.MachineLearning; - messaging(): admin.messaging.Messaging; - projectManagement(): admin.projectManagement.ProjectManagement; - remoteConfig(): admin.remoteConfig.RemoteConfig; - securityRules(): admin.securityRules.SecurityRules; - storage(): admin.storage.Storage; - - /** - * Renders this local `FirebaseApp` unusable and frees the resources of - * all associated services (though it does *not* clean up any backend - * resources). When running the SDK locally, this method - * must be called to ensure graceful termination of the process. - * - * @example - * ```javascript - * app.delete() - * .then(function() { - * console.log("App deleted successfully"); - * }) - * .catch(function(error) { - * console.log("Error deleting app:", error); - * }); - * ``` - */ - delete(): Promise; - } -} - -declare namespace admin.auth { - export import UserMetadata = _auth.admin.auth.UserMetadata; - export import UserInfo = _auth.admin.auth.UserInfo; - export import UserRecord = _auth.admin.auth.UserRecord; - export import UpdateRequest = _auth.admin.auth.UpdateRequest; - export import CreateRequest = _auth.admin.auth.CreateRequest; - export import DecodedIdToken = _auth.admin.auth.DecodedIdToken; - export import ListUsersResult = _auth.admin.auth.ListUsersResult; - export import HashAlgorithmType = _auth.admin.auth.HashAlgorithmType; - export import UserImportOptions = _auth.admin.auth.UserImportOptions; - export import UserImportResult = _auth.admin.auth.UserImportResult; - export import UserImportRecord = _auth.admin.auth.UserImportRecord; - export import SessionCookieOptions = _auth.admin.auth.SessionCookieOptions; - export import ActionCodeSettings = _auth.admin.auth.ActionCodeSettings; - export import Tenant = _auth.admin.auth.Tenant; - export import UpdateTenantRequest = _auth.admin.auth.UpdateTenantRequest; - export import CreateTenantRequest = _auth.admin.auth.CreateTenantRequest; - export import ListTenantsResult = _auth.admin.auth.ListTenantsResult; - export import AuthProviderConfigFilter = _auth.admin.auth.AuthProviderConfigFilter; - export import AuthProviderConfig = _auth.admin.auth.AuthProviderConfig; - export import SAMLAuthProviderConfig = _auth.admin.auth.SAMLAuthProviderConfig; - export import OIDCAuthProviderConfig = _auth.admin.auth.OIDCAuthProviderConfig; - export import SAMLUpdateAuthProviderRequest = _auth.admin.auth.SAMLUpdateAuthProviderRequest; - export import OIDCUpdateAuthProviderRequest = _auth.admin.auth.OIDCUpdateAuthProviderRequest; - export import ListProviderConfigResults = _auth.admin.auth.ListProviderConfigResults; - export import UpdateAuthProviderRequest = _auth.admin.auth.UpdateAuthProviderRequest; - export import BaseAuth = _auth.admin.auth.BaseAuth; - export import TenantAwareAuth = _auth.admin.auth.TenantAwareAuth; - export import Auth = _auth.admin.auth.Auth; - export import TenantManager = _auth.admin.auth.TenantManager; - export import MultiFactorInfo = _auth.admin.auth.MultiFactorInfo; - export import PhoneMultiFactorInfo = _auth.admin.auth.PhoneMultiFactorInfo; - export import CreateMultiFactorInfoRequest = _auth.admin.auth.CreateMultiFactorInfoRequest; - export import CreatePhoneMultiFactorInfoRequest = _auth.admin.auth.CreatePhoneMultiFactorInfoRequest; - export import UpdateMultiFactorInfoRequest = _auth.admin.auth.UpdateMultiFactorInfoRequest; - export import UpdatePhoneMultiFactorInfoRequest = _auth.admin.auth.UpdatePhoneMultiFactorInfoRequest; - export import MultiFactorCreateSettings = _auth.admin.auth.MultiFactorCreateSettings; - export import MultiFactorUpdateSettings = _auth.admin.auth.MultiFactorUpdateSettings; - export import DeleteUsersResult = _auth.admin.auth.DeleteUsersResult; - export import GetUsersResult = _auth.admin.auth.GetUsersResult; -} - -declare namespace admin.credential { - - /** - * Interface that provides Google OAuth2 access tokens used to authenticate - * with Firebase services. - * - * In most cases, you will not need to implement this yourself and can instead - * use the default implementations provided by - * {@link admin.credential `admin.credential`}. - */ - interface Credential { - - /** - * Returns a Google OAuth2 access token object used to authenticate with - * Firebase services. - * - * This object contains the following properties: - * * `access_token` (`string`): The actual Google OAuth2 access token. - * * `expires_in` (`number`): The number of seconds from when the token was - * issued that it expires. - * - * @return A Google OAuth2 access token object. - */ - getAccessToken(): Promise; - } - - - /** - * Returns a credential created from the - * {@link - * https://developers.google.com/identity/protocols/application-default-credentials - * Google Application Default Credentials} - * that grants admin access to Firebase services. This credential can be used - * in the call to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * Google Application Default Credentials are available on any Google - * infrastructure, such as Google App Engine and Google Compute Engine. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * admin.initializeApp({ - * credential: admin.credential.applicationDefault(), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param {!Object=} httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return {!admin.credential.Credential} A credential authenticated via Google - * Application Default Credentials that can be used to initialize an app. - */ - function applicationDefault(httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided service account that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a service account key JSON file - * var serviceAccount = require("path/to/serviceAccountKey.json"); - * admin.initializeApp({ - * credential: admin.credential.cert(serviceAccount), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @example - * ```javascript - * // Providing a service account object inline - * admin.initializeApp({ - * credential: admin.credential.cert({ - * projectId: "", - * clientEmail: "foo@.iam.gserviceaccount.com", - * privateKey: "-----BEGIN PRIVATE KEY----------END PRIVATE KEY-----\n" - * }), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param serviceAccountPathOrObject The path to a service - * account key JSON file or an object representing a service account key. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function cert(serviceAccountPathOrObject: string | admin.ServiceAccount, httpAgent?: Agent): admin.credential.Credential; - - /** - * Returns a credential created from the provided refresh token that grants - * admin access to Firebase services. This credential can be used in the call - * to - * {@link - * https://firebase.google.com/docs/reference/admin/node/admin#.initializeApp - * `admin.initializeApp()`}. - * - * See - * {@link - * https://firebase.google.com/docs/admin/setup#initialize_the_sdk - * Initialize the SDK} - * for more details. - * - * @example - * ```javascript - * // Providing a path to a refresh token JSON file - * var refreshToken = require("path/to/refreshToken.json"); - * admin.initializeApp({ - * credential: admin.credential.refreshToken(refreshToken), - * databaseURL: "https://.firebaseio.com" - * }); - * ``` - * - * @param refreshTokenPathOrObject The path to a Google - * OAuth2 refresh token JSON file or an object representing a Google OAuth2 - * refresh token. - * @param httpAgent Optional [HTTP Agent](https://nodejs.org/api/http.html#http_class_http_agent) - * to be used when retrieving access tokens from Google token servers. - * - * @return A credential authenticated via the - * provided service account that can be used to initialize an app. - */ - function refreshToken(refreshTokenPathOrObject: string | Object, httpAgent?: Agent): admin.credential.Credential; -} - -declare namespace admin.database { - export import Database = _database.admin.database.Database; - export import DataSnapshot = _database.admin.database.DataSnapshot; - export import OnDisconnect = _database.admin.database.OnDisconnect; - export import EventType = _database.admin.database.EventType; - export import Query = _database.admin.database.Query; - export import Reference = _database.admin.database.Reference; - export import ThenableReference = _database.admin.database.ThenableReference; - export import enableLogging = _database.admin.database.enableLogging; - export import ServerValue = _database.admin.database.ServerValue; -} - -declare namespace admin.messaging { - export import Message = _messaging.admin.messaging.Message; - export import MulticastMessage = _messaging.admin.messaging.MulticastMessage; - export import AndroidConfig = _messaging.admin.messaging.AndroidConfig; - export import AndroidNotification = _messaging.admin.messaging.AndroidNotification; - export import LightSettings = _messaging.admin.messaging.LightSettings; - export import AndroidFcmOptions = _messaging.admin.messaging.AndroidFcmOptions; - export import ApnsConfig = _messaging.admin.messaging.ApnsConfig; - export import ApnsPayload = _messaging.admin.messaging.ApnsPayload; - export import Aps = _messaging.admin.messaging.Aps; - export import ApsAlert = _messaging.admin.messaging.ApsAlert; - export import CriticalSound = _messaging.admin.messaging.CriticalSound; - export import ApnsFcmOptions = _messaging.admin.messaging.ApnsFcmOptions; - export import FcmOptions = _messaging.admin.messaging.FcmOptions; - export import Notification = _messaging.admin.messaging.Notification; - export import WebpushConfig = _messaging.admin.messaging.WebpushConfig; - export import WebpushFcmOptions = _messaging.admin.messaging.WebpushFcmOptions; - export import WebpushNotification = _messaging.admin.messaging.WebpushNotification; - export import MessagingTopicManagementResponse = _messaging.admin.messaging.MessagingTopicManagementResponse; - export import BatchResponse = _messaging.admin.messaging.BatchResponse; - export import SendResponse = _messaging.admin.messaging.SendResponse; - export import Messaging = _messaging.admin.messaging.Messaging; - - // Legacy API types. - export import DataMessagePayload = _messaging.admin.messaging.DataMessagePayload; - export import NotificationMessagePayload = _messaging.admin.messaging.NotificationMessagePayload; - export import MessagingPayload = _messaging.admin.messaging.MessagingPayload; - export import MessagingOptions = _messaging.admin.messaging.MessagingOptions; - export import MessagingDevicesResponse = _messaging.admin.messaging.MessagingDevicesResponse; - export import MessagingDeviceResult = _messaging.admin.messaging.MessagingDeviceResult; - export import MessagingDeviceGroupResponse = _messaging.admin.messaging.MessagingDeviceGroupResponse; - export import MessagingTopicResponse = _messaging.admin.messaging.MessagingTopicResponse; - export import MessagingConditionResponse = _messaging.admin.messaging.MessagingConditionResponse; -} - -declare namespace admin.storage { - - /** - * The default `Storage` service if no - * app is provided or the `Storage` service associated with the provided - * app. - */ - interface Storage { - /** - * Optional app whose `Storage` service to - * return. If not provided, the default `Storage` service will be returned. - */ - app: admin.app.App; - /** - * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) - * instance as defined in the `@google-cloud/storage` package. - */ - bucket(name?: string): Bucket; - } -} - -declare namespace admin.firestore { - export import v1beta1 = _firestore.v1beta1; - export import v1 = _firestore.v1; - - export import CollectionReference = _firestore.CollectionReference; - export import DocumentData = _firestore.DocumentData; - export import DocumentReference = _firestore.DocumentReference; - export import DocumentSnapshot = _firestore.DocumentSnapshot; - export import FieldPath = _firestore.FieldPath; - export import FieldValue = _firestore.FieldValue; - export import Firestore = _firestore.Firestore; - export import GeoPoint = _firestore.GeoPoint; - export import Query = _firestore.Query; - export import QueryDocumentSnapshot = _firestore.QueryDocumentSnapshot; - export import QuerySnapshot = _firestore.QuerySnapshot; - export import Timestamp = _firestore.Timestamp; - export import Transaction = _firestore.Transaction; - export import WriteBatch = _firestore.WriteBatch; - export import WriteResult = _firestore.WriteResult; - - export import setLogFunction = _firestore.setLogFunction; -} - -declare namespace admin.instanceId { - export import InstanceId = _instanceId.admin.instanceId.InstanceId; -} - -declare namespace admin.projectManagement { - export import ShaCertificate = _projectManagement.admin.projectManagement.ShaCertificate; - export import AppMetadata = _projectManagement.admin.projectManagement.AppMetadata; - export import AppPlatform = _projectManagement.admin.projectManagement.AppPlatform; - export import AndroidAppMetadata = _projectManagement.admin.projectManagement.AndroidAppMetadata; - export import IosAppMetadata = _projectManagement.admin.projectManagement.IosAppMetadata; - export import AndroidApp = _projectManagement.admin.projectManagement.AndroidApp; - export import IosApp = _projectManagement.admin.projectManagement.IosApp; - export import ProjectManagement = _projectManagement.admin.projectManagement.ProjectManagement; -} - -declare namespace admin.remoteConfig { - - /** - * Colors that are associated with conditions for display purposes. - */ - type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | - 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; - - /** - * Interface representing a Remote Config template. - */ - interface RemoteConfigTemplate { - /** - * A list of conditions in descending order by priority. - */ - conditions: RemoteConfigCondition[]; - - /** - * Map of parameter keys to their optional default values and optional conditional values. - */ - parameters: { [key: string]: RemoteConfigParameter }; - - /** - * Map of parameter group names to their parameter group objects. - * A group's name is mutable but must be unique among groups in the Remote Config template. - * The name is limited to 256 characters and intended to be human-readable. Any Unicode - * characters are allowed. - */ - parameterGroups: { [key: string]: RemoteConfigParameterGroup }; - - /** - * ETag of the current Remote Config template (readonly). - */ - readonly etag: string; - - /** - * Version information for the current Remote Config template. - */ - version?: Version; - } - - /** - * Interface representing a Remote Config parameter. - * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the - * parameter to have any effect. - */ - interface RemoteConfigParameter { - - /** - * The value to set the parameter to, when none of the named conditions evaluate to `true`. - */ - defaultValue?: RemoteConfigParameterValue; - - /** - * A `(condition name, value)` map. The condition name of the highest priority - * (the one listed first in the Remote Config template's conditions list) determines the value of - * this parameter. - */ - conditionalValues?: { [key: string]: RemoteConfigParameterValue }; - - /** - * A description for this parameter. Should not be over 100 characters and may contain any - * Unicode characters. - */ - description?: string; - } - - /** - * Interface representing a Remote Config parameter group. - * Grouping parameters is only for management purposes and does not affect client-side - * fetching of parameter values. - */ - export interface RemoteConfigParameterGroup { - /** - * A description for the group. Its length must be less than or equal to 256 characters. - * A description may contain any Unicode characters. - */ - description?: string; - - /** - * Map of parameter keys to their optional default values and optional conditional values for - * parameters that belong to this group. A parameter only appears once per - * Remote Config template. An ungrouped parameter appears at the top level, whereas a - * parameter organized within a group appears within its group's map of parameters. - */ - parameters: { [key: string]: RemoteConfigParameter }; - } - - /** - * Interface representing a Remote Config condition. - * A condition targets a specific group of users. A list of these conditions make up - * part of a Remote Config template. - */ - interface RemoteConfigCondition { - - /** - * A non-empty and unique name of this condition. - */ - name: string; - - /** - * The logic of this condition. - * See the documentation on - * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} - * for the expected syntax of this field. - */ - expression: string; - - /** - * The color associated with this condition for display purposes in the Firebase Console. - * Not specifying this value results in the console picking an arbitrary color to associate - * with the condition. - */ - tagColor?: TagColor; - } - - /** - * Interface representing an explicit parameter value. - */ - interface ExplicitParameterValue { - /** - * The `string` value that the parameter is set to. - */ - value: string; - } - - /** - * Interface representing an in-app-default value. - */ - interface InAppDefaultValue { - /** - * If `true`, the parameter is omitted from the parameter values returned to a client. - */ - useInAppDefault: boolean; - } - - /** - * Type representing a Remote Config parameter value. - * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or - * an `InAppDefaultValue`. - */ - type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; - - /** - * Interface representing a Remote Config template version. - * Output only, except for the version description. Contains metadata about a particular - * version of the Remote Config template. All fields are set at the time the specified Remote - * Config template is published. A version's description field may be specified in - * `publishTemplate` calls. - */ - export interface Version { - /** - * The version number of a Remote Config template. - */ - versionNumber?: string; - - /** - * The timestamp of when this version of the Remote Config template was written to the - * Remote Config backend. - */ - updateTime?: string; - - /** - * The origin of the template update action. - */ - updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | - 'REST_API' | 'ADMIN_SDK_NODE'); - - /** - * The type of the template update action. - */ - updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | - 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); - - /** - * Aggregation of all metadata fields about the account that performed the update. - */ - updateUser?: RemoteConfigUser; - - /** - * The user-provided description of the corresponding Remote Config template. - */ - description?: string; - - /** - * The version number of the Remote Config template that has become the current version - * due to a rollback. Only present if this version is the result of a rollback. - */ - rollbackSource?: string; - - /** - * Indicates whether this Remote Config template was published before version history was - * supported. - */ - isLegacy?: boolean; - } - - /** Interface representing a list of Remote Config template versions. */ - export interface ListVersionsResult { - /** - * A list of version metadata objects, sorted in reverse chronological order. - */ - versions: Version[]; - - /** - * Token to retrieve the next page of results, or empty if there are no more results - * in the list. - */ - nextPageToken?: string; - } - - /** Interface representing options for Remote Config list versions operation. */ - export interface ListVersionsOptions { - /** - * The maximum number of items to return per page. - */ - pageSize?: number; - - /** - * The `nextPageToken` value returned from a previous list versions request, if any. - */ - pageToken?: string; - - /** - * Specifies the newest version number to include in the results. - * If specified, must be greater than zero. Defaults to the newest version. - */ - endVersionNumber?: string | number; - - /** - * Specifies the earliest update time to include in the results. Any entries updated before this - * time are omitted. - */ - startTime?: Date | string; - - /** - * Specifies the latest update time to include in the results. Any entries updated on or after - * this time are omitted. - */ - endTime?: Date | string; - } - - /** Interface representing a Remote Config user.*/ - export interface RemoteConfigUser { - /** - * Email address. Output only. - */ - email: string; - - /** - * Display name. Output only. - */ - name?: string; - - /** - * Image URL. Output only. - */ - imageUrl?: string; - } - - /** - * The Firebase `RemoteConfig` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.remoteConfig()`](admin.remoteConfig#remoteConfig). - */ - interface RemoteConfig { - app: admin.app.App; - - /** - * Gets the current active version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplate(): Promise; - - /** - * Gets the requested version of the {@link admin.remoteConfig.RemoteConfigTemplate - * `RemoteConfigTemplate`} of the project. - * - * @param versionNumber Version number of the Remote Config template to look up. - * - * @return A promise that fulfills with a `RemoteConfigTemplate`. - */ - getTemplateAtVersion(versionNumber: number | string): Promise; - - /** - * Validates a {@link admin.remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. - * - * @param template The Remote Config template to be validated. - * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. - */ - validateTemplate(template: RemoteConfigTemplate): Promise; - - /** - * Publishes a Remote Config template. - * - * @param template The Remote Config template to be published. - * @param options Optional options object when publishing a Remote Config template: - * - {boolean} `force` Setting this to `true` forces the Remote Config template to - * be updated and circumvent the ETag. This approach is not recommended - * because it risks causing the loss of updates to your Remote Config - * template if multiple clients are updating the Remote Config template. - * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates - * ETag usage and forced updates}. - * - * @return A Promise that fulfills with the published `RemoteConfigTemplate`. - */ - publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; - - /** - * Rolls back a project's published Remote Config template to the specified version. - * A rollback is equivalent to getting a previously published Remote Config - * template and re-publishing it using a force update. - * - * @param versionNumber The version number of the Remote Config template to roll back to. - * The specified version number must be lower than the current version number, and not have - * been deleted due to staleness. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * @return A promise that fulfills with the published `RemoteConfigTemplate`. - */ - rollback(versionNumber: string | number): Promise; - - /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse - * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (that is, all except the - * template that is being fetched by clients) are also deleted if they are more than 90 days old. - * - * @param options Optional {@link admin.remoteConfig.ListVersionsOptions `ListVersionsOptions`} - * object for getting a list of template versions. - * @return A promise that fulfills with a `ListVersionsResult`. - */ - listVersions(options?: ListVersionsOptions): Promise; - - /** - * Creates and returns a new Remote Config template from a JSON string. - * - * @param json The JSON string to populate a Remote Config template. - * - * @return A new template instance. - */ - createTemplateFromJSON(json: string): RemoteConfigTemplate; - } -} - -declare namespace admin.securityRules { - export import RulesFile = _securityRules.admin.securityRules.RulesFile; - export import RulesetMetadata = _securityRules.admin.securityRules.RulesetMetadata; - export import Ruleset = _securityRules.admin.securityRules.Ruleset; - export import RulesetMetadataList = _securityRules.admin.securityRules.RulesetMetadataList; - export import SecurityRules = _securityRules.admin.securityRules.SecurityRules; -} - -declare namespace admin.machineLearning { - /** - * Interface representing options for listing Models. - */ - interface ListModelsOptions { - /** - * An expression that specifies how to filter the results. - * - * Examples: - * - * ``` - * display_name = your_model - * display_name : experimental_* - * tags: face_detector AND tags: experimental - * state.published = true - * ``` - * - * See https://firebase.google.com/docs/ml-kit/manage-hosted-models#list_your_projects_models - */ - filter?: string; - - /** The number of results to return in each page. */ - pageSize?: number; - - /** A token that specifies the result page to return. */ - pageToken?: string; - } - - /** Response object for a listModels operation. */ - interface ListModelsResult { - /** A list of models in your project. */ - readonly models: Model[]; - - /** - * A token you can use to retrieve the next page of results. If null, the - * current page is the final page. - */ - readonly pageToken?: string; - } - - /** - * A TensorFlow Lite Model output object - */ - interface TFLiteModel { - /** The size of the model. */ - readonly sizeBytes: number; - - /** The URI from which the model was originally provided to Firebase. */ - readonly gcsTfliteUri?: string; - } - - /** - * A Firebase ML Model input object - */ - interface ModelOptions { - /** A name for the model. This is the name you use from your app to load the model. */ - displayName?: string; - - /** Tags for easier model management. */ - tags?: string[]; - - /** - * An object containing the URI of the model in Cloud Storage. - * - * Example: `tfliteModel: { gcsTfliteUri: 'gs://your-bucket/your-model.tflite' }` - */ - tfliteModel?: { gcsTfliteUri: string }; - } - - /** - * A Firebase ML Model output object - */ - interface Model { - /** The ID of the model. */ - readonly modelId: string; - - /** The model's name. This is the name you use from your app to load the model. */ - readonly displayName: string; - - /** The model's tags. */ - readonly tags?: string[]; - - /** The timestamp of the model's creation. */ - readonly createTime: string; - - /** The timestamp of the model's most recent update. */ - readonly updateTime: string; - - /** Error message when model validation fails. */ - readonly validationError?: string; - - /** True if the model is published. */ - readonly published: boolean; - - /** - * The ETag identifier of the current version of the model. This value - * changes whenever you update any of the model's properties. - */ - readonly etag: string; - - /** - * The hash of the model's `tflite` file. This value changes only when - * you upload a new TensorFlow Lite model. - */ - readonly modelHash?: string; - - /** - * True if the model is locked by a server-side operation. You can't make - * changes to a locked model. See {@link waitForUnlocked `waitForUnlocked()`}. - */ - readonly locked: boolean; - - /** - * Wait for the model to be unlocked. - * - * @param {number} maxTimeSeconds The maximum time in seconds to wait. - * - * @return {Promise} A promise that resolves when the model is unlocked - * or the maximum wait time has passed. - */ - waitForUnlocked(maxTimeSeconds?: number): Promise; - - /** Metadata about the model's TensorFlow Lite model file. */ - readonly tfliteModel?: TFLiteModel; - } - - /** - * The Firebase `MachineLearning` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.machineLearning()`](admin.machineLearning#machineLearning). - */ - interface MachineLearning { - /** - * The {@link admin.app.App} associated with the current `MachineLearning` - * service instance. - */ - app: admin.app.App; - - /** - * Creates a model in Firebase ML. - * - * @param {ModelOptions} model The model to create. - * - * @return {Promise} A Promise fulfilled with the created model. - */ - createModel(model: ModelOptions): Promise; - - /** - * Updates a model in Firebase ML. - * - * @param {string} modelId The ID of the model to update. - * @param {ModelOptions} model The model fields to update. - * - * @return {Promise} A Promise fulfilled with the updated model. - */ - updateModel(modelId: string, model: ModelOptions): Promise; - - /** - * Publishes a model in Firebase ML. - * - * @param {string} modelId The ID of the model to publish. - * - * @return {Promise} A Promise fulfilled with the published model. - */ - publishModel(modelId: string): Promise; - - /** - * Unpublishes a model in Firebase ML. - * - * @param {string} modelId The ID of the model to unpublish. - * - * @return {Promise} A Promise fulfilled with the unpublished model. - */ - unpublishModel(modelId: string): Promise; - - /** - * Gets a model from Firebase ML. - * - * @param {string} modelId The ID of the model to get. - * - * @return {Promise} A Promise fulfilled with the model object. - */ - getModel(modelId: string): Promise; - - /** - * Lists models from Firebase ML. - * - * @param {ListModelsOptions} options The listing options. - * - * @return {Promise} A promise that - * resolves with the current (filtered) list of models and the next page - * token. For the last page, an empty list of models and no page token - * are returned. - */ - listModels(options?: ListModelsOptions): Promise; - - /** - * Deletes a model from Firebase ML. - * - * @param {string} modelId The ID of the model to delete. - */ - deleteModel(modelId: string): Promise; - } -} +import * as admin from './firebase-namespace'; declare module 'firebase-admin' { } diff --git a/src/index.ts b/src/index.ts index 9374b5fd8c..eb121bc023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/installations/index.ts b/src/installations/index.ts new file mode 100644 index 0000000000..373b904c9c --- /dev/null +++ b/src/installations/index.ts @@ -0,0 +1,85 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link installations.Installations `Installations`} service for the + * default app or a given app. + * + * `admin.installations()` can be called with no arguments to access the default + * app's {@link installations.Installations `Installations`} service or as + * `admin.installations(app)` to access the + * {@link installations.Installations `Installations`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Installations service for the default app + * var defaultInstallations = admin.installations(); + * ``` + * + * @example + * ```javascript + * // Get the Installations service for a given app + * var otherInstallations = admin.installations(otherApp); + *``` + * + * @param app Optional app whose `Installations` service to + * return. If not provided, the default `Installations` service is + * returned. + * + * @return The default `Installations` service if + * no app is provided or the `Installations` service associated with the + * provided app. + */ +export declare function installations(app?: app.App): installations.Installations; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace installations { + /** + * Gets the {@link Installations `Installations`} service for the + * current app. + * + * @example + * ```javascript + * var installations = app.installations(); + * // The above is shorthand for: + * // var installations = admin.installations(app); + * ``` + * + * @return The `Installations` service for the + * current app. + */ + export interface Installations { + app: app.App; + + /** + * Deletes the specified installation ID and the associated data from Firebase. + * + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase installation ID does + * not delete Analytics data. See + * [Delete a Firebase installation](/docs/projects/manage-installations#delete-installation) + * for more information. + * + * @param fid The Firebase installation ID to be deleted. + * + * @return A promise fulfilled when the installation ID is deleted. + */ + deleteInstallation(fid: string): Promise; + } +} diff --git a/src/instance-id/instance-id-request.ts b/src/installations/installations-request-handler.ts similarity index 70% rename from src/instance-id/instance-id-request.ts rename to src/installations/installations-request-handler.ts index b683ff038b..299fd3136e 100644 --- a/src/instance-id/instance-id-request.ts +++ b/src/installations/installations-request-handler.ts @@ -1,5 +1,6 @@ /*! - * Copyright 2017 Google Inc. + * @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. @@ -14,8 +15,8 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error'; +import { FirebaseApp } from '../firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; import { ApiSettings, AuthorizedHttpClient, HttpRequestConfig, HttpError, } from '../utils/api-request'; @@ -32,10 +33,10 @@ const FIREBASE_IID_TIMEOUT = 10000; /** HTTP error codes raised by the backend server. */ const ERROR_CODES: {[key: number]: string} = { - 400: 'Malformed instance ID argument.', + 400: 'Malformed installation ID argument.', 401: 'Request not authorized.', - 403: 'Project does not match instance ID or the client does not have sufficient privileges.', - 404: 'Failed to find the instance ID.', + 403: 'Project does not match installation ID or the client does not have sufficient privileges.', + 404: 'Failed to find the installation ID.', 409: 'Already deleted.', 429: 'Request throttled out by the backend server.', 500: 'Internal server error.', @@ -43,9 +44,9 @@ const ERROR_CODES: {[key: number]: string} = { }; /** - * Class that provides mechanism to send requests to the Firebase Instance ID backend endpoints. + * Class that provides mechanism to send requests to the FIS backend endpoints. */ -export class FirebaseInstanceIdRequestHandler { +export class FirebaseInstallationsRequestHandler { private readonly host: string = FIREBASE_IID_HOST; private readonly timeout: number = FIREBASE_IID_TIMEOUT; @@ -53,7 +54,7 @@ export class FirebaseInstanceIdRequestHandler { private path: string; /** - * @param {FirebaseApp} app The app used to fetch access tokens to sign API requests. + * @param app The app used to fetch access tokens to sign API requests. * * @constructor */ @@ -61,21 +62,21 @@ export class FirebaseInstanceIdRequestHandler { this.httpClient = new AuthorizedHttpClient(app); } - public deleteInstanceId(instanceId: string): Promise { - if (!validator.isNonEmptyString(instanceId)) { - return Promise.reject(new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_INSTANCE_ID, - 'Instance ID must be a non-empty string.', + public deleteInstallation(fid: string): Promise { + if (!validator.isNonEmptyString(fid)) { + return Promise.reject(new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_INSTALLATION_ID, + 'Installation ID must be a non-empty string.', )); } - return this.invokeRequestHandler(new ApiSettings(instanceId, 'DELETE')); + return this.invokeRequestHandler(new ApiSettings(fid, 'DELETE')); } /** * Invokes the request handler based on the API settings object passed. * - * @param {ApiSettings} apiSettings The API endpoint settings to apply to request and response. - * @return {Promise} A promise that resolves when the request is complete. + * @param apiSettings The API endpoint settings to apply to request and response. + * @return A promise that resolves when the request is complete. */ private invokeRequestHandler(apiSettings: ApiSettings): Promise { return this.getPathPrefix() @@ -97,8 +98,8 @@ export class FirebaseInstanceIdRequestHandler { response.data.error : response.text; const template: string = ERROR_CODES[response.status]; const message: string = template ? - `Instance ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage; - throw new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR, message); + `Installation ID "${apiSettings.getEndpoint()}": ${template}` : errorMessage; + throw new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR, message); } // In case of timeouts and other network errors, the HttpClient returns a // FirebaseError wrapped in the response. Simply throw it here. @@ -115,9 +116,9 @@ export class FirebaseInstanceIdRequestHandler { .then((projectId) => { if (!validator.isNonEmptyString(projectId)) { // Assert for an explicit projct ID (either via AppOptions or the cert itself). - throw new FirebaseInstanceIdError( - InstanceIdClientErrorCode.INVALID_PROJECT_ID, - 'Failed to determine project ID for InstanceId. Initialize the ' + throw new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_PROJECT_ID, + 'Failed to determine project ID for Installations. Initialize the ' + 'SDK with service account credentials or set project ID as an app option. ' + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', ); diff --git a/src/installations/installations.ts b/src/installations/installations.ts new file mode 100644 index 0000000000..a55e5af295 --- /dev/null +++ b/src/installations/installations.ts @@ -0,0 +1,68 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp } from '../firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../utils/error'; +import { FirebaseInstallationsRequestHandler } from './installations-request-handler'; +import { installations } from './index'; +import * as validator from '../utils/validator'; + +import InstallationsInterface = installations.Installations; + +/** + * The `Installations` service for the current app. + */ +export class Installations implements InstallationsInterface { + + private app_: FirebaseApp; + private requestHandler: FirebaseInstallationsRequestHandler; + + /** + * @param app The app for this Installations service. + * @constructor + */ + constructor(app: FirebaseApp) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseInstallationsError( + InstallationsClientErrorCode.INVALID_ARGUMENT, + 'First argument passed to admin.installations() must be a valid Firebase app instance.', + ); + } + + this.app_ = app; + this.requestHandler = new FirebaseInstallationsRequestHandler(app); + } + + /** + * Deletes the specified installation ID and the associated data from Firebase. + * + * @param fid The Firebase installation ID to be deleted. + * + * @return A promise fulfilled when the installation ID is deleted. + */ + public deleteInstallation(fid: string): Promise { + return this.requestHandler.deleteInstallation(fid); + } + + /** + * Returns the app associated with this Installations instance. + * + * @return The app associated with this Installations instance. + */ + get app(): FirebaseApp { + return this.app_; + } +} diff --git a/src/instance-id.d.ts b/src/instance-id.d.ts deleted file mode 100644 index 616d0d193e..0000000000 --- a/src/instance-id.d.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as _admin from './index.d'; - -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -export namespace admin.instanceId { - /** - * Gets the {@link InstanceId `InstanceId`} service for the - * current app. - * - * @example - * ```javascript - * var instanceId = app.instanceId(); - * // The above is shorthand for: - * // var instanceId = admin.instanceId(app); - * ``` - * - * @return The `InstanceId` service for the - * current app. - */ - interface InstanceId { - app: _admin.app.App; - - /** - * Deletes the specified instance ID and the associated data from Firebase. - * - * Note that Google Analytics for Firebase uses its own form of Instance ID to - * keep track of analytics data. Therefore deleting a Firebase Instance ID does - * not delete Analytics data. See - * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) - * for more information. - * - * @param instanceId The instance ID to be deleted. - * - * @return A promise fulfilled when the instance ID is deleted. - */ - deleteInstanceId(instanceId: string): Promise; - } -} diff --git a/src/instance-id/index.ts b/src/instance-id/index.ts new file mode 100644 index 0000000000..a5ea77eb8b --- /dev/null +++ b/src/instance-id/index.ts @@ -0,0 +1,87 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link instanceId.InstanceId `InstanceId`} service for the + * default app or a given app. + * + * `admin.instanceId()` can be called with no arguments to access the default + * app's {@link instanceId.InstanceId `InstanceId`} service or as + * `admin.instanceId(app)` to access the + * {@link instanceId.InstanceId `InstanceId`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Instance ID service for the default app + * var defaultInstanceId = admin.instanceId(); + * ``` + * + * @example + * ```javascript + * // Get the Instance ID service for a given app + * var otherInstanceId = admin.instanceId(otherApp); + *``` + * + * 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. + * + * @return The default `InstanceId` service if + * no app is provided or the `InstanceId` service associated with the + * provided app. + * + * @deprecated + */ +export declare function instanceId(app?: app.App): instanceId.InstanceId; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace instanceId { + /** + * The {@link InstanceId `InstanceId`} service for the + * current app. + * + * @deprecated + */ + export interface InstanceId { + app: app.App; + + /** + * Deletes the specified instance ID and the associated data from Firebase. + * + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase Instance ID does + * not delete Analytics data. See + * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) + * for more information. + * + * This API is deprecated. Developers are advised to use the `Installations.deleteInstallation()` + * API instead. + * + * @param instanceId The instance ID to be deleted. + * + * @return A promise fulfilled when the instance ID is deleted. + * + * @deprecated + */ + deleteInstanceId(instanceId: string): Promise; + } +} diff --git a/src/instance-id/instance-id.ts b/src/instance-id/instance-id.ts index 27e807528d..80937ffc0c 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,36 +14,35 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseInstanceIdError, InstanceIdClientErrorCode} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {FirebaseInstanceIdRequestHandler} from './instance-id-request'; - +import { FirebaseApp } from '../firebase-app'; +import { + FirebaseInstallationsError, FirebaseInstanceIdError, InstallationsClientErrorCode, InstanceIdClientErrorCode, +} from '../utils/error'; +import { instanceId } from './index'; import * as validator from '../utils/validator'; +import InstanceIdInterface = instanceId.InstanceId; + /** - * Internals of an InstanceId service instance. + * Gets the {@link InstanceId `InstanceId`} service for the + * current app. + * + * @example + * ```javascript + * var instanceId = app.instanceId(); + * // The above is shorthand for: + * // var instanceId = admin.instanceId(app); + * ``` + * + * @return The `InstanceId` service for the + * current app. */ -class InstanceIdInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} - -export class InstanceId implements FirebaseServiceInterface { - public INTERNAL: InstanceIdInternals = new InstanceIdInternals(); +export class InstanceId implements InstanceIdInterface { private app_: FirebaseApp; - private requestHandler: FirebaseInstanceIdRequestHandler; /** - * @param {FirebaseApp} app The app for this InstanceId service. + * @param app The app for this InstanceId service. * @constructor */ constructor(app: FirebaseApp) { @@ -55,28 +54,41 @@ export class InstanceId implements FirebaseServiceInterface { } this.app_ = app; - this.requestHandler = new FirebaseInstanceIdRequestHandler(app); } /** - * Deletes the specified instance ID from Firebase. This can be used to delete an instance ID - * and associated user data from a Firebase project, pursuant to the General Data Protection - * Regulation (GDPR). + * Deletes the specified instance ID and the associated data from Firebase. + * + * Note that Google Analytics for Firebase uses its own form of Instance ID to + * keep track of analytics data. Therefore deleting a Firebase Instance ID does + * not delete Analytics data. See + * [Delete an Instance ID](/support/privacy/manage-iids#delete_an_instance_id) + * for more information. * - * @param {string} instanceId The instance ID to be deleted - * @return {Promise} A promise that resolves when the instance ID is successfully deleted. + * @param instanceId The instance ID to be deleted. + * + * @return A promise fulfilled when the instance ID is deleted. */ public deleteInstanceId(instanceId: string): Promise { - return this.requestHandler.deleteInstanceId(instanceId) - .then(() => { - // Return nothing on success + return this.app.installations().deleteInstallation(instanceId) + .catch((err) => { + if (err instanceof FirebaseInstallationsError) { + let code = err.code.replace('installations/', ''); + if (code === InstallationsClientErrorCode.INVALID_INSTALLATION_ID.code) { + code = InstanceIdClientErrorCode.INVALID_INSTANCE_ID.code; + } + + throw new FirebaseInstanceIdError({ code, message: err.message }); + } + + throw err; }); } /** * Returns the app associated with this InstanceId instance. * - * @return {FirebaseApp} The app associated with this InstanceId instance. + * @return The app associated with this InstanceId instance. */ get app(): FirebaseApp { return this.app_; diff --git a/src/machine-learning/index.ts b/src/machine-learning/index.ts new file mode 100644 index 0000000000..7e5a8078cd --- /dev/null +++ b/src/machine-learning/index.ts @@ -0,0 +1,282 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link machineLearning.MachineLearning `MachineLearning`} service for the + * default app or a given app. + * + * `admin.machineLearning()` can be called with no arguments to access the + * default app's {@link machineLearning.MachineLearning + * `MachineLearning`} service or as `admin.machineLearning(app)` to access + * the {@link machineLearning.MachineLearning `MachineLearning`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the MachineLearning service for the default app + * var defaultMachineLearning = admin.machineLearning(); + * ``` + * + * @example + * ```javascript + * // Get the MachineLearning service for a given app + * var otherMachineLearning = admin.machineLearning(otherApp); + * ``` + * + * @param app Optional app whose `MachineLearning` service to + * return. If not provided, the default `MachineLearning` service + * will be returned. + * + * @return The default `MachineLearning` service if no app is provided or the + * `MachineLearning` service associated with the provided app. + */ +export declare function machineLearning(app?: app.App): machineLearning.MachineLearning; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace machineLearning { + /** + * Firebase ML Model input objects + */ + export interface ModelOptionsBase { + displayName?: string; + tags?: string[]; + } + + export interface GcsTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + gcsTfliteUri: string; + }; + } + + export interface AutoMLTfliteModelOptions extends ModelOptionsBase { + tfliteModel: { + automlModel: string; + }; + } + + export type ModelOptions = ModelOptionsBase | GcsTfliteModelOptions | AutoMLTfliteModelOptions; + + /** + * A TensorFlow Lite Model output object + * + * One of either the `gcsTfliteUri` or `automlModel` properties will be + * defined. + */ + export interface TFLiteModel { + /** The size of the model. */ + readonly sizeBytes: number; + + /** The URI from which the model was originally provided to Firebase. */ + readonly gcsTfliteUri?: string; + /** + * The AutoML model reference from which the model was originally provided + * to Firebase. + */ + readonly automlModel?: string; + } + + /** + * A Firebase ML Model output object + */ + export interface Model { + /** The ID of the model. */ + readonly modelId: string; + + /** + * The model's name. This is the name you use from your app to load the + * model. + */ + readonly displayName: string; + + /** + * The model's tags, which can be used to group or filter models in list + * operations. + */ + readonly tags?: string[]; + + /** The timestamp of the model's creation. */ + readonly createTime: string; + + /** The timestamp of the model's most recent update. */ + readonly updateTime: string; + + /** Error message when model validation fails. */ + readonly validationError?: string; + + /** True if the model is published. */ + readonly published: boolean; + + /** + * The ETag identifier of the current version of the model. This value + * changes whenever you update any of the model's properties. + */ + readonly etag: string; + + /** + * The hash of the model's `tflite` file. This value changes only when + * you upload a new TensorFlow Lite model. + */ + readonly modelHash?: string; + + /** + * True if the model is locked by a server-side operation. You can't make + * changes to a locked model. See {@link waitForUnlocked `waitForUnlocked()`}. + */ + readonly locked: boolean; + + /** + * Wait for the model to be unlocked. + * + * @param {number} maxTimeMillis The maximum time in milliseconds to wait. + * If not specified, a default maximum of 2 minutes is used. + * + * @return {Promise} A promise that resolves when the model is unlocked + * or the maximum wait time has passed. + */ + waitForUnlocked(maxTimeMillis?: number): Promise; + + /** + * Return the model as a JSON object. + */ + toJSON(): {[key: string]: any}; + + /** Metadata about the model's TensorFlow Lite model file. */ + readonly tfliteModel?: TFLiteModel; + } + + /** + * Interface representing options for listing Models. + */ + export interface ListModelsOptions { + /** + * An expression that specifies how to filter the results. + * + * Examples: + * + * ``` + * display_name = your_model + * display_name : experimental_* + * tags: face_detector AND tags: experimental + * state.published = true + * ``` + * + * See https://firebase.google.com/docs/ml/manage-hosted-models#list_your_projects_models + */ + filter?: string; + + /** The number of results to return in each page. */ + pageSize?: number; + + /** A token that specifies the result page to return. */ + pageToken?: string; + } + + /** Response object for a listModels operation. */ + export interface ListModelsResult { + /** A list of models in your project. */ + readonly models: Model[]; + + /** + * A token you can use to retrieve the next page of results. If null, the + * current page is the final page. + */ + readonly pageToken?: string; + } + + + /** + * The Firebase `MachineLearning` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.machineLearning()`](admin.machineLearning#machineLearning). + */ + export interface MachineLearning { + /** + * The {@link app.App} associated with the current `MachineLearning` + * service instance. + */ + app: app.App; + + /** + * Creates a model in the current Firebase project. + * + * @param {ModelOptions} model The model to create. + * + * @return {Promise} A Promise fulfilled with the created model. + */ + createModel(model: ModelOptions): Promise; + + /** + * Updates a model's metadata or model file. + * + * @param {string} modelId The ID of the model to update. + * @param {ModelOptions} model The model fields to update. + * + * @return {Promise} A Promise fulfilled with the updated model. + */ + updateModel(modelId: string, model: ModelOptions): Promise; + + /** + * Publishes a Firebase ML model. + * + * A published model can be downloaded to client apps. + * + * @param {string} modelId The ID of the model to publish. + * + * @return {Promise} A Promise fulfilled with the published model. + */ + publishModel(modelId: string): Promise; + + /** + * Unpublishes a Firebase ML model. + * + * @param {string} modelId The ID of the model to unpublish. + * + * @return {Promise} A Promise fulfilled with the unpublished model. + */ + unpublishModel(modelId: string): Promise; + + /** + * Gets the model specified by the given ID. + * + * @param {string} modelId The ID of the model to get. + * + * @return {Promise} A Promise fulfilled with the model object. + */ + getModel(modelId: string): Promise; + + /** + * Lists the current project's models. + * + * @param {ListModelsOptions} options The listing options. + * + * @return {Promise} A promise that + * resolves with the current (filtered) list of models and the next page + * token. For the last page, an empty list of models and no page token + * are returned. + */ + listModels(options?: ListModelsOptions): Promise; + + /** + * Deletes a model from the current project. + * + * @param {string} modelId The ID of the model to delete. + */ + deleteModel(modelId: string): Promise; + } +} diff --git a/src/machine-learning/machine-learning-api-client.ts b/src/machine-learning/machine-learning-api-client.ts index 56b47118c7..2281b84d53 100644 --- a/src/machine-learning/machine-learning-api-client.ts +++ b/src/machine-learning/machine-learning-api-client.ts @@ -14,42 +14,41 @@ * limitations under the License. */ -import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient } from '../utils/api-request'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, ExponentialBackoffPoller +} from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; import { FirebaseMachineLearningError, MachineLearningErrorCode } from './machine-learning-utils'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../firebase-app'; +import { machineLearning } from './index'; + +import GcsTfliteModelOptions = machineLearning.GcsTfliteModelOptions; +import ListModelsOptions = machineLearning.ListModelsOptions; +import ModelOptions = machineLearning.ModelOptions; const ML_V1BETA2_API = 'https://firebaseml.googleapis.com/v1beta2'; const FIREBASE_VERSION_HEADER = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, }; +// Operation polling defaults +const POLL_DEFAULT_MAX_TIME_MILLISECONDS = 120000; // Maximum overall 2 minutes +const POLL_BASE_WAIT_TIME_MILLISECONDS = 3000; // Start with 3 second delay +const POLL_MAX_WAIT_TIME_MILLISECONDS = 30000; // Maximum 30 second delay + export interface StatusErrorResponse { readonly code: number; readonly message: string; } -/** - * A Firebase ML Model input object - */ -export interface ModelOptions { - displayName?: string; - tags?: string[]; - tfliteModel?: { gcsTfliteUri: string }; -} +export type ModelUpdateOptions = ModelOptions & { state?: { published?: boolean }}; -export interface ModelUpdateOptions extends ModelOptions { - state?: { published?: boolean }; -} - -/** Interface representing listModels options. */ -export interface ListModelsOptions { - filter?: string; - pageSize?: number; - pageToken?: string; +export function isGcsTfliteModelOptions(options: ModelOptions): options is GcsTfliteModelOptions { + const gcsUri = (options as GcsTfliteModelOptions)?.tfliteModel?.gcsTfliteUri; + return typeof gcsUri !== 'undefined' } export interface ModelContent { @@ -60,7 +59,9 @@ export interface ModelContent { readonly published?: boolean; }; readonly tfliteModel?: { - readonly gcsTfliteUri: string; + readonly gcsTfliteUri?: string; + readonly automlModel?: string; + readonly sizeBytes: number; }; } @@ -71,6 +72,7 @@ export interface ModelResponse extends ModelContent { readonly updateTime: string; readonly etag: string; readonly modelHash?: string; + readonly activeOperations?: OperationResponse[]; } export interface ListModelsResponse { @@ -80,6 +82,7 @@ export interface ListModelsResponse { export interface OperationResponse { readonly name?: string; + readonly metadata?: {[key: string]: any}; readonly done: boolean; readonly error?: StatusErrorResponse; readonly response?: ModelResponse; @@ -112,7 +115,7 @@ export class MachineLearningApiClient { const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model content.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'POST', @@ -130,7 +133,7 @@ export class MachineLearningApiClient { const err = new FirebaseMachineLearningError('invalid-argument', 'Invalid model or mask content.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'PATCH', @@ -141,14 +144,20 @@ export class MachineLearningApiClient { }); } - public getModel(modelId: string): Promise { return Promise.resolve() .then(() => { return this.getModelName(modelId); }) .then((modelName) => { - return this.getResource(modelName); + return this.getResourceWithShortName(modelName); + }); + } + + public getOperation(operationName: string): Promise { + return Promise.resolve() + .then(() => { + return this.getResourceWithFullName(operationName); }); } @@ -177,7 +186,7 @@ export class MachineLearningApiClient { 'invalid-argument', 'Next page token must be a non-empty string.'); return Promise.reject(err); } - return this.getUrl() + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', @@ -189,7 +198,7 @@ export class MachineLearningApiClient { } public deleteModel(modelId: string): Promise { - return this.getUrl() + return this.getProjectUrl() .then((url) => { const modelName = this.getModelName(modelId); const request: HttpRequestConfig = { @@ -200,15 +209,93 @@ export class MachineLearningApiClient { }); } + /** + * Handles a Long Running Operation coming back from the server. + * + * @param op The operation to handle + * @param options The options for polling + */ + public handleOperation( + op: OperationResponse, + options?: { + wait?: boolean; + maxTimeMillis?: number; + baseWaitMillis?: number; + maxWaitMillis?: number; + }): + Promise { + if (op.done) { + if (op.response) { + return Promise.resolve(op.response); + } else if (op.error) { + const err = FirebaseMachineLearningError.fromOperationError( + op.error.code, op.error.message); + return Promise.reject(err); + } + + // Done operations must have either a response or an error. + throw new FirebaseMachineLearningError('invalid-server-response', + 'Invalid operation response.'); + } + + // Operation is not done + if (options?.wait) { + return this.pollOperationWithExponentialBackoff(op.name!, options); + } + + const metadata = op.metadata || {}; + const metadataType: string = metadata['@type'] || ''; + if (!metadataType.includes('ModelOperationMetadata')) { + throw new FirebaseMachineLearningError('invalid-server-response', + `Unknown Metadata type: ${JSON.stringify(metadata)}`); + } + + return this.getModel(extractModelId(metadata.name)); + } + + // baseWaitMillis and maxWaitMillis should only ever be modified by unit tests to run faster. + private pollOperationWithExponentialBackoff( + opName: string, + options?: { + maxTimeMillis?: number; + baseWaitMillis?: number; + maxWaitMillis?: number; + }): Promise { + + const maxTimeMilliseconds = options?.maxTimeMillis ?? POLL_DEFAULT_MAX_TIME_MILLISECONDS; + const baseWaitMillis = options?.baseWaitMillis ?? POLL_BASE_WAIT_TIME_MILLISECONDS; + const maxWaitMillis = options?.maxWaitMillis ?? POLL_MAX_WAIT_TIME_MILLISECONDS; + + const poller = new ExponentialBackoffPoller( + baseWaitMillis, + maxWaitMillis, + maxTimeMilliseconds); + + return poller.poll(() => { + return this.getOperation(opName) + .then((responseData: {[key: string]: any}) => { + if (!responseData.done) { + return null; + } + if (responseData.error) { + const err = FirebaseMachineLearningError.fromOperationError( + responseData.error.code, responseData.error.message); + throw err; + } + return responseData.response; + }); + }); + } + /** * Gets the specified resource from the ML API. Resource names must be the short names without project * ID prefix (e.g. `models/123456789`). * - * @param {string} name Full qualified name of the resource to get. + * @param {string} name Short name of the resource to get. e.g. 'models/12345' * @returns {Promise} A promise that fulfills with the resource. */ - private getResource(name: string): Promise { - return this.getUrl() + private getResourceWithShortName(name: string): Promise { + return this.getProjectUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', @@ -218,6 +305,20 @@ export class MachineLearningApiClient { }); } + /** + * Gets the specified resource from the ML API. Resource names must be the full names including project + * number prefix. + * @param fullName Full resource name of the resource to get. e.g. projects/123465/operations/987654 + * @returns {Promise} A promise that fulfulls with the resource. + */ + private getResourceWithFullName(fullName: string): Promise { + const request: HttpRequestConfig = { + method: 'GET', + url: `${ML_V1BETA2_API}/${fullName}` + }; + return this.sendRequest(request); + } + private sendRequest(request: HttpRequestConfig): Promise { request.headers = FIREBASE_VERSION_HEADER; return this.httpClient.send(request) @@ -250,7 +351,7 @@ export class MachineLearningApiClient { return new FirebaseMachineLearningError(code, message); } - private getUrl(): Promise { + private getProjectUrl(): Promise { return this.getProjectIdPrefix() .then((projectIdPrefix) => { return `${ML_V1BETA2_API}/${projectIdPrefix}`; @@ -309,3 +410,7 @@ const ERROR_CODE_MAPPING: {[key: string]: MachineLearningErrorCode} = { UNAUTHENTICATED: 'authentication-error', UNKNOWN: 'unknown-error', }; + +function extractModelId(resourceName: string): string { + return resourceName.split('/').pop()!; +} diff --git a/src/machine-learning/machine-learning.ts b/src/machine-learning/machine-learning.ts index 2879e5d460..dcbb72caec 100644 --- a/src/machine-learning/machine-learning.ts +++ b/src/machine-learning/machine-learning.ts @@ -14,44 +14,28 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {MachineLearningApiClient, ModelResponse, OperationResponse, - ModelOptions, ModelUpdateOptions, ListModelsOptions} from './machine-learning-api-client'; -import {FirebaseError} from '../utils/error'; - +import { FirebaseApp } from '../firebase-app'; +import { + MachineLearningApiClient, ModelResponse, ModelUpdateOptions, isGcsTfliteModelOptions +} from './machine-learning-api-client'; +import { FirebaseError } from '../utils/error'; import * as validator from '../utils/validator'; -import {FirebaseMachineLearningError} from './machine-learning-utils'; +import { FirebaseMachineLearningError } from './machine-learning-utils'; import { deepCopy } from '../utils/deep-copy'; import * as utils from '../utils'; +import { machineLearning } from './index'; -/** - * Internals of an ML instance. - */ -class MachineLearningInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise} An empty Promise that will be resolved when the - * service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} - -/** Response object for a listModels operation. */ -export interface ListModelsResult { - models: Model[]; - pageToken?: string; -} +import ListModelsOptions = machineLearning.ListModelsOptions; +import ListModelsResult = machineLearning.ListModelsResult; +import MachineLearningInterface = machineLearning.MachineLearning; +import ModelInterface = machineLearning.Model; +import ModelOptions = machineLearning.ModelOptions; +import TFLiteModel = machineLearning.TFLiteModel; /** * The Firebase Machine Learning class */ -export class MachineLearning implements FirebaseServiceInterface { - public readonly INTERNAL = new MachineLearningInternals(); +export class MachineLearning implements MachineLearningInterface { private readonly client: MachineLearningApiClient; private readonly appInternal: FirebaseApp; @@ -92,7 +76,8 @@ export class MachineLearning implements FirebaseServiceInterface { public createModel(model: ModelOptions): Promise { return this.signUrlIfPresent(model) .then((modelContent) => this.client.createModel(modelContent)) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -107,7 +92,8 @@ export class MachineLearning implements FirebaseServiceInterface { const updateMask = utils.generateUpdateMask(model); return this.signUrlIfPresent(model) .then((modelContent) => this.client.updateModel(modelId, modelContent, updateMask)) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -141,7 +127,7 @@ export class MachineLearning implements FirebaseServiceInterface { */ public getModel(modelId: string): Promise { return this.client.getModel(modelId) - .then((modelResponse) => new Model(modelResponse)); + .then((modelResponse) => new Model(modelResponse, this.client)); } /** @@ -164,9 +150,9 @@ export class MachineLearning implements FirebaseServiceInterface { } let models: Model[] = []; if (resp.models) { - models = resp.models.map((rs) => new Model(rs)); + models = resp.models.map((rs) => new Model(rs, this.client)); } - const result: ListModelsResult = {models}; + const result: { models: Model[]; pageToken?: string } = { models }; if (resp.nextPageToken) { result.pageToken = resp.nextPageToken; } @@ -185,17 +171,18 @@ export class MachineLearning implements FirebaseServiceInterface { private setPublishStatus(modelId: string, publish: boolean): Promise { const updateMask = ['state.published']; - const options: ModelUpdateOptions = {state: {published: publish}}; + const options: ModelUpdateOptions = { state: { published: publish } }; return this.client.updateModel(modelId, options, updateMask) - .then((operation) => handleOperation(operation)); + .then((operation) => this.client.handleOperation(operation)) + .then((modelResponse) => new Model(modelResponse, this.client)); } private signUrlIfPresent(options: ModelOptions): Promise { const modelOptions = deepCopy(options); - if (modelOptions.tfliteModel?.gcsTfliteUri) { + if (isGcsTfliteModelOptions(modelOptions)) { return this.signUrl(modelOptions.tfliteModel.gcsTfliteUri) - .then ((uri: string) => { - modelOptions.tfliteModel!.gcsTfliteUri = uri; + .then((uri: string) => { + modelOptions.tfliteModel.gcsTfliteUri = uri; return modelOptions; }) .catch((err: Error) => { @@ -229,95 +216,147 @@ export class MachineLearning implements FirebaseServiceInterface { } } - /** * A Firebase ML Model output object. */ -export class Model { - public readonly modelId: string; - public readonly displayName: string; - public readonly tags?: string[]; - public readonly createTime: string; - public readonly updateTime: string; - public readonly validationError?: string; - public readonly published: boolean; - public readonly etag: string; - public readonly modelHash?: string; - - public readonly tfliteModel?: TFLiteModel; - - constructor(model: ModelResponse) { - if (!validator.isNonNullObject(model) || - !validator.isNonEmptyString(model.name) || - !validator.isNonEmptyString(model.createTime) || - !validator.isNonEmptyString(model.updateTime) || - !validator.isNonEmptyString(model.displayName) || - !validator.isNonEmptyString(model.etag)) { - throw new FirebaseMachineLearningError( - 'invalid-server-response', - `Invalid Model response: ${JSON.stringify(model)}`); - } +export class Model implements ModelInterface { + private model: ModelResponse; + private readonly client?: MachineLearningApiClient; + + constructor(model: ModelResponse, client: MachineLearningApiClient) { + this.model = Model.validateAndClone(model); + this.client = client; + } + + get modelId(): string { + return extractModelId(this.model.name); + } + + get displayName(): string { + return this.model.displayName!; + } + + get tags(): string[] { + return this.model.tags || []; + } + + get createTime(): string { + return new Date(this.model.createTime).toUTCString(); + } - this.modelId = extractModelId(model.name); - this.displayName = model.displayName; - this.tags = model.tags || []; - this.createTime = new Date(model.createTime).toUTCString(); - this.updateTime = new Date(model.updateTime).toUTCString(); - if (model.state?.validationError?.message) { - this.validationError = model.state?.validationError?.message; + get updateTime(): string { + return new Date(this.model.updateTime).toUTCString(); + } + + get validationError(): string | undefined { + return this.model.state?.validationError?.message; + } + + get published(): boolean { + return this.model.state?.published || false; + } + + get etag(): string { + return this.model.etag; + } + + get modelHash(): string | undefined { + return this.model.modelHash; + } + + get tfliteModel(): TFLiteModel | undefined { + // Make a copy so people can't directly modify the private this.model object. + return deepCopy(this.model.tfliteModel); + } + + /** + * Locked indicates if there are active long running operations on the model. + * Models may not be modified when they are locked. + */ + public get locked(): boolean { + return (this.model.activeOperations?.length ?? 0) > 0; + } + + public toJSON(): {[key: string]: any} { + // We can't just return this.model because it has extra fields and + // different formats etc. So we build the expected model object. + const jsonModel: {[key: string]: any} = { + modelId: this.modelId, + displayName: this.displayName, + tags: this.tags, + createTime: this.createTime, + updateTime: this.updateTime, + published: this.published, + etag: this.etag, + locked: this.locked, + }; + + // Also add possibly undefined fields if they exist. + + if (this.validationError) { + jsonModel['validationError'] = this.validationError; } - this.published = model.state?.published || false; - this.etag = model.etag; - if (model.modelHash) { - this.modelHash = model.modelHash; + + if (this.modelHash) { + jsonModel['modelHash'] = this.modelHash; } - if (model.tfliteModel) { - this.tfliteModel = { - gcsTfliteUri: model.tfliteModel.gcsTfliteUri, - sizeBytes: model.tfliteModel.sizeBytes, - }; + + if (this.tfliteModel) { + jsonModel['tfliteModel'] = this.tfliteModel; } - } - public get locked(): boolean { - // Backend does not currently return locked models. - // This will likely change in future. - return false; + return jsonModel; } - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - public waitForUnlocked(maxTimeSeconds?: number): Promise { - // Backend does not currently return locked models. - // This will likely change in future. + + /** + * Wait for the active operations on the model to complete. + * @param maxTimeMillis The number of milliseconds to wait for the model to be unlocked. If unspecified, + * a default will be used. + */ + public waitForUnlocked(maxTimeMillis?: number): Promise { + if ((this.model.activeOperations?.length ?? 0) > 0) { + // The client will always be defined on Models that have activeOperations + // because models with active operations came back from the server and + // were constructed with a non-empty client. + return this.client!.handleOperation(this.model.activeOperations![0], { wait: true, maxTimeMillis }) + .then((modelResponse) => { + this.model = Model.validateAndClone(modelResponse); + }); + } return Promise.resolve(); } -} -/** - * A TFLite Model output object - */ -export interface TFLiteModel { - readonly sizeBytes: number; + 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); - readonly gcsTfliteUri: string; + // If tflite Model is specified, it must have a source consisting of + // oneof {gcsTfliteUri, automlModel} + if (model.tfliteModel && + !validator.isNonEmptyString(model.tfliteModel.gcsTfliteUri) && + !validator.isNonEmptyString(model.tfliteModel.automlModel)) { + // If we have some other source, ignore the whole tfliteModel. + delete (tmpModel as any).tfliteModel; + } + + // Remove '@type' field. We don't need it. + if ((tmpModel as any)['@type']) { + delete (tmpModel as any)['@type']; + } + return tmpModel; + } } function extractModelId(resourceName: string): string { return resourceName.split('/').pop()!; } - -function handleOperation(op: OperationResponse): Model { - // Backend currently does not return operations that are not done. - if (op.done) { - // Done operations must have either a response or an error. - if (op.response) { - return new Model(op.response); - } else if (op.error) { - throw FirebaseMachineLearningError.fromOperationError( - op.error.code, op.error.message); - } - } - throw new FirebaseMachineLearningError( - 'invalid-server-response', - `Invalid Operation response: ${JSON.stringify(op)}`); -} diff --git a/src/messaging/batch-request.ts b/src/messaging/batch-request-internal.ts similarity index 100% rename from src/messaging/batch-request.ts rename to src/messaging/batch-request-internal.ts diff --git a/src/messaging.d.ts b/src/messaging/index.ts similarity index 85% rename from src/messaging.d.ts rename to src/messaging/index.ts index ff134002ab..0ed93e01e9 100644 --- a/src/messaging.d.ts +++ b/src/messaging/index.ts @@ -1,302 +1,278 @@ -import * as _admin from './index.d'; - -type BaseMessage = { - data?: { [key: string]: string }; - notification?: admin.messaging.Notification; - android?: admin.messaging.AndroidConfig; - webpush?: admin.messaging.WebpushConfig; - apns?: admin.messaging.ApnsConfig; - fcmOptions?: admin.messaging.FcmOptions; -}; - -interface TokenMessage extends BaseMessage { - token: string; -} - -interface TopicMessage extends BaseMessage { - topic: string; -} +/*! + * 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, FirebaseError, FirebaseArrayIndexError } from '../firebase-namespace-api'; + +/** + * Gets the {@link messaging.Messaging `Messaging`} service for the + * default app or a given app. + * + * `admin.messaging()` can be called with no arguments to access the default + * app's {@link messaging.Messaging `Messaging`} service or as + * `admin.messaging(app)` to access the + * {@link messaging.Messaging `Messaging`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Messaging service for the default app + * var defaultMessaging = admin.messaging(); + * ``` + * + * @example + * ```javascript + * // Get the Messaging service for a given app + * var otherMessaging = admin.messaging(otherApp); + * ``` + * + * @param app Optional app whose `Messaging` service to + * return. If not provided, the default `Messaging` service will be returned. + * + * @return The default `Messaging` service if no + * app is provided or the `Messaging` service associated with the provided + * app. + */ +export declare function messaging(app?: app.App): messaging.Messaging; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace messaging { + interface BaseMessage { + data?: { [key: string]: string }; + notification?: Notification; + android?: AndroidConfig; + webpush?: WebpushConfig; + apns?: ApnsConfig; + fcmOptions?: FcmOptions; + } -interface ConditionMessage extends BaseMessage { - condition: string; -} + export interface TokenMessage extends BaseMessage { + token: string; + } -export namespace admin.messaging { - type Message = TokenMessage | TopicMessage | ConditionMessage; + export interface TopicMessage extends BaseMessage { + topic: string; + } - interface MulticastMessage extends BaseMessage { - tokens: string[]; + export interface ConditionMessage extends BaseMessage { + condition: string; } /** - * Represents the Android-specific options that can be included in an - * {@link admin.messaging.Message}. + * 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. */ - 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'); + export type Message = TokenMessage | TopicMessage | ConditionMessage; - /** - * Time-to-live duration of the message in milliseconds. - */ - ttl?: number; - - /** - * Package name of the application where the registration tokens must match - * in order to receive the message. - */ - restrictedPackageName?: string; - - /** - * A collection of data fields to be included in the message. All values must - * be strings. When provided, overrides any data fields set on the top-level - * `admin.messaging.Message`.} - */ - data?: { [key: string]: string }; - - /** - * Android notification to be included in the message. - */ - notification?: AndroidNotification; - - /** - * Options for features provided by the FCM SDK for Android. - */ - fcmOptions?: AndroidFcmOptions; + /** + * Payload for the `admin.messaging.sendMulticast()` method. The payload contains all the fields + * in the `BaseMessage` type, and a list of tokens. + */ + export interface MulticastMessage extends BaseMessage { + tokens: string[]; } /** - * Represents the Android-specific notification options that can be included in - * {@link admin.messaging.AndroidConfig}. + * A notification that can be included in {@link messaging.Message}. */ - interface AndroidNotification { - + export interface Notification { /** - * Title of the Android notification. When provided, overrides the title set via - * `admin.messaging.Notification`. + * The title of the notification. */ title?: string; - /** - * Body of the Android notification. When provided, overrides the body set via - * `admin.messaging.Notification`. + * The notification body */ body?: string; - /** - * Icon resource for the Android notification. + * URL of an image to be displayed in the notification. */ - icon?: string; + imageUrl?: string; + } + /** + * Represents platform-independent options for features provided by the FCM SDKs. + */ + export interface FcmOptions { /** - * Notification icon color in `#rrggbb` format. + * The label associated with the message's analytics data. */ - color?: string; + analyticsLabel?: string; + } - /** - * File name of the sound to be played when the device receives the - * notification. - */ - sound?: string; + /** + * Represents the WebPush protocol options that can be included in an + * {@link messaging.Message}. + */ + export interface WebpushConfig { /** - * Notification tag. This is an identifier used to replace existing - * notifications in the notification drawer. If not specified, each request - * creates a new notification. + * A collection of WebPush headers. Header values must be strings. + * + * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) + * for supported headers. */ - tag?: string; + headers?: { [key: string]: string }; /** - * URL of an image to be displayed in the notification. + * A collection of data fields. */ - imageUrl?: string; + data?: { [key: string]: 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. + * A WebPush notification payload to be included in the message. */ - clickAction?: string; + notification?: WebpushNotification; /** - * Key of the body string in the app's string resource to use to localize the - * body text. - * + * Options for features provided by the FCM SDK for Web. */ - bodyLocKey?: string; + fcmOptions?: WebpushFcmOptions; + } - /** - * An array of resource keys that will be used in place of the format - * specifiers in `bodyLocKey`. - */ - bodyLocArgs?: string[]; + /** Represents options for features provided by the FCM SDK for Web + * (which are not part of the Webpush standard). + */ + export interface WebpushFcmOptions { /** - * Key of the title string in the app's string resource to use to localize the - * title text. + * The link to open when the user clicks on the notification. + * For all URL values, HTTPS is required. */ - titleLocKey?: string; + link?: string; + } - /** - * An array of resource keys that will be used in place of the format - * specifiers in `titleLocKey`. - */ - titleLocArgs?: string[]; + /** + * Represents the WebPush-specific notification options that can be included in + * {@link messaging.WebpushConfig}. This supports most of the standard + * options as defined in the Web Notification + * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). + */ + export interface WebpushNotification { /** - * 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. + * Title text of the notification. */ - channelId?: string; + title?: 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. + * An array of notification actions representing the actions + * available to the user when the notification is presented. */ - ticker?: string; + actions?: Array<{ - /** - * 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; + /** + * An action available to the user when the notification is presented + */ + action: string; - /** - * 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; + /** + * Optional icon for a notification action. + */ + icon?: string; + + /** + * Title of the notification action. + */ + title: string; + }>; /** - * Sets whether or not this notification is relevant only to the current device. - * Some notifications can be bridged to other devices for remote display, such as - * a Wear OS watch. This hint can be set to recommend this notification not be bridged. - * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) + * URL of the image used to represent the notification when there is + * not enough space to display the notification itself. */ - localOnly?: boolean; + badge?: string; /** - * 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. + * Body text of the notification. */ - priority?: ('min' | 'low' | 'default' | 'high' | 'max'); + body?: string; /** - * 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`. + * Arbitrary data that you want associated with the notification. + * This can be of any data type. */ - vibrateTimingsMillis?: number[]; + data?: any; /** - * If set to `true`, use the Android framework's default vibrate pattern for the - * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, - * the default value is used instead of the user-specified `vibrate_timings`. + * The direction in which to display the notification. Must be one + * of `auto`, `ltr` or `rtl`. */ - defaultVibrateTimings?: boolean; + dir?: 'auto' | 'ltr' | 'rtl'; /** - * If set to `true`, use the Android framework's default sound for the notification. - * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * URL to the notification icon. */ - defaultSound?: boolean; + icon?: string; /** - * 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. + * URL of an image to be displayed in the notification. */ - lightSettings?: LightSettings; + image?: string; /** - * If set to `true`, use the Android framework's default LED light settings - * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). - * If `default_light_settings` is set to `true` and `light_settings` is also set, - * the user-specified `light_settings` is used instead of the default value. + * The notification's language as a BCP 47 language tag. */ - defaultLightSettings?: boolean; + lang?: string; /** - * Sets the visibility of the notification. Must be either `private`, `public`, - * or `secret`. If unspecified, defaults to `private`. + * A boolean specifying whether the user should be notified after a + * new notification replaces an old one. Defaults to false. */ - visibility?: ('private' | 'public' | 'secret'); + renotify?: boolean; /** - * Sets the number of items this notification represents. May be displayed as a - * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). - * For example, this might be useful if you're using just one notification to - * represent multiple new messages but you want the count here to represent - * the number of total new messages. If zero or unspecified, systems - * that support badging use the default, which is to increment a number - * displayed on the long-press menu each time a new notification arrives. + * Indicates that a notification should remain active until the user + * clicks or dismisses it, rather than closing automatically. + * Defaults to false. */ - notificationCount?: number; - } + requireInteraction?: boolean; - /** - * Represents settings to control notification LED that can be included in - * {@link admin.messaging.AndroidNotification}. - */ - interface LightSettings { /** - * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. + * A boolean specifying whether the notification should be silent. + * Defaults to false. */ - color: string; + silent?: boolean; /** - * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. + * An identifying tag for the notification. */ - lightOnDurationMillis: number; + tag?: string; /** - * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. + * Timestamp of the notification. Refer to + * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp + * for details. */ - lightOffDurationMillis: number; - } - - /** - * Represents options for features provided by the FCM SDK for Android. - */ - interface AndroidFcmOptions { + timestamp?: number; /** - * The label associated with the message's analytics data. + * A vibration pattern for the device's vibration hardware to emit + * when the notification fires. */ - analyticsLabel?: string; + vibrate?: number | number[]; + [key: string]: any; } /** * Represents the APNs-specific options that can be included in an - * {@link admin.messaging.Message}. Refer to + * {@link messaging.Message}. Refer to * [Apple documentation](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html) * for various headers and payload fields supported by APNs. */ - interface ApnsConfig { - + export interface ApnsConfig { /** * A collection of APNs headers. Header values must be strings. */ @@ -312,23 +288,25 @@ export namespace admin.messaging { */ fcmOptions?: ApnsFcmOptions; } + /** * Represents the payload of an APNs message. Mainly consists of the `aps` * dictionary. But may also contain other arbitrary custom keys. */ - interface ApnsPayload { + export interface ApnsPayload { /** * The `aps` dictionary to be included in the message. */ aps: Aps; - [customData: string]: object; + [customData: string]: any; } + /** * Represents the [aps dictionary](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html) * that is part of APNs messages. */ - interface Aps { + export interface Aps { /** * Alert to be included in the message. This may be a string or an object of @@ -370,7 +348,7 @@ export namespace admin.messaging { [customData: string]: any; } - interface ApsAlert { + export interface ApsAlert { title?: string; subtitle?: string; body?: string; @@ -388,7 +366,7 @@ export namespace admin.messaging { * Represents a critical sound configuration that can be included in the * `aps` dictionary of an APNs payload. */ - interface CriticalSound { + export interface CriticalSound { /** * The critical alert flag. Set to `true` to enable the critical alert. @@ -412,7 +390,7 @@ export namespace admin.messaging { /** * Represents options for features provided by the FCM SDK for iOS. */ - interface ApnsFcmOptions { + export interface ApnsFcmOptions { /** * The label associated with the message's analytics data. @@ -425,188 +403,267 @@ export namespace admin.messaging { imageUrl?: string; } + /** - * Represents platform-independent options for features provided by the FCM SDKs. + * Represents the Android-specific options that can be included in an + * {@link messaging.Message}. */ - interface FcmOptions { + export interface AndroidConfig { + + /** + * Collapse key for the message. Collapse key serves as an identifier for a + * group of messages that can be collapsed, so that only the last message gets + * sent when delivery can be resumed. A maximum of four different collapse keys + * may be active at any given time. + */ + collapseKey?: string; + + /** + * Priority of the message. Must be either `normal` or `high`. + */ + priority?: ('high' | 'normal'); + + /** + * Time-to-live duration of the message in milliseconds. + */ + ttl?: number; + + /** + * Package name of the application where the registration tokens must match + * in order to receive the message. + */ + restrictedPackageName?: string; + + /** + * A collection of data fields to be included in the message. All values must + * be strings. When provided, overrides any data fields set on the top-level + * `admin.messaging.Message`.} + */ + data?: { [key: string]: string }; + + /** + * Android notification to be included in the message. + */ + notification?: AndroidNotification; + + /** + * Options for features provided by the FCM SDK for Android. + */ + fcmOptions?: AndroidFcmOptions; + } + + /** + * Represents the Android-specific notification options that can be included in + * {@link messaging.AndroidConfig}. + */ + export interface AndroidNotification { + /** + * Title of the Android notification. When provided, overrides the title set via + * `admin.messaging.Notification`. + */ + title?: string; + + /** + * Body of the Android notification. When provided, overrides the body set via + * `admin.messaging.Notification`. + */ + body?: string; /** - * The label associated with the message's analytics data. + * Icon resource for the Android notification. */ - analyticsLabel?: string; - } + icon?: string; + /** + * Notification icon color in `#rrggbb` format. + */ + color?: string; - /** - * A notification that can be included in {@link admin.messaging.Message}. - */ - interface Notification { /** - * The title of the notification. + * File name of the sound to be played when the device receives the + * notification. */ - title?: string; + sound?: string; + /** - * The notification body + * Notification tag. This is an identifier used to replace existing + * notifications in the notification drawer. If not specified, each request + * creates a new notification. */ - body?: string; + tag?: string; + /** * URL of an image to be displayed in the notification. */ imageUrl?: string; - } - /** - * Represents the WebPush protocol options that can be included in an - * {@link admin.messaging.Message}. - */ - interface WebpushConfig { /** - * A collection of WebPush headers. Header values must be strings. - * - * See [WebPush specification](https://tools.ietf.org/html/rfc8030#section-5) - * for supported headers. + * 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. */ - headers?: { [key: string]: string }; + clickAction?: string; /** - * A collection of data fields. + * Key of the body string in the app's string resource to use to localize the + * body text. + * */ - data?: { [key: string]: string }; + bodyLocKey?: string; /** - * A WebPush notification payload to be included in the message. + * An array of resource keys that will be used in place of the format + * specifiers in `bodyLocKey`. */ - notification?: WebpushNotification; + bodyLocArgs?: string[]; /** - * Options for features provided by the FCM SDK for Web. + * Key of the title string in the app's string resource to use to localize the + * title text. */ - fcmOptions?: WebpushFcmOptions; - } - - /** Represents options for features provided by the FCM SDK for Web - * (which are not part of the Webpush standard). - */ - interface WebpushFcmOptions { + titleLocKey?: string; /** - * The link to open when the user clicks on the notification. - * For all URL values, HTTPS is required. + * An array of resource keys that will be used in place of the format + * specifiers in `titleLocKey`. */ - link?: string; - } - - /** - * Represents the WebPush-specific notification options that can be included in - * {@link admin.messaging.WebpushConfig}. This supports most of the standard - * options as defined in the Web Notification - * [specification](https://developer.mozilla.org/en-US/docs/Web/API/notification/Notification). - */ - interface WebpushNotification { + titleLocArgs?: string[]; /** - * Title text of the notification. + * 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. */ - title?: string; + channelId?: string; /** - * An array of notification actions representing the actions - * available to the user when the notification is presented. + * 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. */ - actions?: Array<{ - - /** - * An action available to the user when the notification is presented - */ - action: string; + ticker?: string; - /** - * Optional icon for a notification action. - */ - icon?: 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; - /** - * Title of the notification action. - */ - title: string; - }>; + /** + * 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; /** - * URL of the image used to represent the notification when there is - * not enough space to display the notification itself. + * Sets whether or not this notification is relevant only to the current device. + * Some notifications can be bridged to other devices for remote display, such as + * a Wear OS watch. This hint can be set to recommend this notification not be bridged. + * See [Wear OS guides](https://developer.android.com/training/wearables/notifications/bridger#existing-method-of-preventing-bridging) */ - badge?: string; + localOnly?: boolean; /** - * Body text of the notification. + * 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. */ - body?: string; + priority?: ('min' | 'low' | 'default' | 'high' | 'max'); /** - * Arbitrary data that you want associated with the notification. - * This can be of any data type. + * 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`. */ - data?: any; + vibrateTimingsMillis?: number[]; /** - * The direction in which to display the notification. Must be one - * of `auto`, `ltr` or `rtl`. + * If set to `true`, use the Android framework's default vibrate pattern for the + * notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_vibrate_timings` is set to `true` and `vibrate_timings` is also set, + * the default value is used instead of the user-specified `vibrate_timings`. */ - dir?: 'auto' | 'ltr' | 'rtl'; + defaultVibrateTimings?: boolean; /** - * URL to the notification icon. + * If set to `true`, use the Android framework's default sound for the notification. + * Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). */ - icon?: string; + defaultSound?: boolean; /** - * URL of an image to be displayed in the notification. + * 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. */ - image?: string; + lightSettings?: LightSettings; /** - * The notification's language as a BCP 47 language tag. + * If set to `true`, use the Android framework's default LED light settings + * for the notification. Default values are specified in [`config.xml`](https://android.googlesource.com/platform/frameworks/base/+/master/core/res/res/values/config.xml). + * If `default_light_settings` is set to `true` and `light_settings` is also set, + * the user-specified `light_settings` is used instead of the default value. */ - lang?: string; + defaultLightSettings?: boolean; /** - * A boolean specifying whether the user should be notified after a - * new notification replaces an old one. Defaults to false. + * Sets the visibility of the notification. Must be either `private`, `public`, + * or `secret`. If unspecified, defaults to `private`. */ - renotify?: boolean; + visibility?: ('private' | 'public' | 'secret'); /** - * Indicates that a notification should remain active until the user - * clicks or dismisses it, rather than closing automatically. - * Defaults to false. + * Sets the number of items this notification represents. May be displayed as a + * badge count for Launchers that support badging. See [`NotificationBadge`(https://developer.android.com/training/notify-user/badges). + * For example, this might be useful if you're using just one notification to + * represent multiple new messages but you want the count here to represent + * the number of total new messages. If zero or unspecified, systems + * that support badging use the default, which is to increment a number + * displayed on the long-press menu each time a new notification arrives. */ - requireInteraction?: boolean; + notificationCount?: number; + } + /** + * Represents settings to control notification LED that can be included in + * {@link messaging.AndroidNotification}. + */ + export interface LightSettings { /** - * A boolean specifying whether the notification should be silent. - * Defaults to false. + * Required. Sets color of the LED in `#rrggbb` or `#rrggbbaa` format. */ - silent?: boolean; + color: string; /** - * An identifying tag for the notification. + * Required. Along with `light_off_duration`, defines the blink rate of LED flashes. */ - tag?: string; + lightOnDurationMillis: number; /** - * Timestamp of the notification. Refer to - * https://developer.mozilla.org/en-US/docs/Web/API/notification/timestamp - * for details. + * Required. Along with `light_on_duration`, defines the blink rate of LED flashes. */ - timestamp?: number; + lightOffDurationMillis: number; + } + + /** + * Represents options for features provided by the FCM SDK for Android. + */ + export interface AndroidFcmOptions { /** - * A vibration pattern for the device's vibration hardware to emit - * when the notification fires. + * The label associated with the message's analytics data. */ - vibrate?: number | number[]; - [key: string]: any; + 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 @@ -619,7 +676,7 @@ export namespace admin.messaging { * See [Build send requests](/docs/cloud-messaging/send-message) * for code samples and detailed documentation. */ - interface DataMessagePayload { + export interface DataMessagePayload { [key: string]: string; } @@ -631,8 +688,8 @@ export namespace admin.messaging { * See [Build send requests](/docs/cloud-messaging/send-message) * for code samples and detailed documentation. */ - interface NotificationMessagePayload { - + export interface NotificationMessagePayload { + /** * Identifier used to replace existing notifications in the notification drawer. * @@ -685,10 +742,10 @@ export namespace admin.messaging { /** * 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. + * "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; @@ -796,18 +853,19 @@ export namespace admin.messaging { * [Build send requests](/docs/cloud-messaging/send-message) * for code samples and detailed documentation. */ - interface MessagingPayload { + export interface MessagingPayload { /** * The data message payload. */ - data?: admin.messaging.DataMessagePayload; + data?: DataMessagePayload; /** * The notification message payload. */ - notification?: admin.messaging.NotificationMessagePayload; + notification?: NotificationMessagePayload; } + /** * Interface representing the options that can be provided when sending a * message via the FCM legacy APIs. @@ -815,7 +873,7 @@ export namespace admin.messaging { * See [Build send requests](/docs/cloud-messaging/send-message) * for code samples and detailed documentation. */ - interface MessagingOptions { + export interface MessagingOptions { /** * Whether or not the message should actually be sent. When set to `true`, @@ -906,20 +964,12 @@ export namespace admin.messaging { [key: string]: any | undefined; } - /** - * Interface representing the status of a message sent to an individual device - * via the FCM legacy APIs. - * - * See - * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) - * for code samples and detailed documentation. - */ - interface MessagingDeviceResult { - + /* Individual status response payload from single devices */ + export interface MessagingDeviceResult { /** * The error that occurred when processing the message for the recipient. */ - error?: _admin.FirebaseError; + error?: FirebaseError; /** * A unique ID for the successfully processed message. @@ -935,60 +985,31 @@ export namespace admin.messaging { } /** - * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDevice `sendToDevice()`} method. + * Interface representing the status of a message sent to an individual device + * via the FCM legacy APIs. * * See * [Send to individual devices](/docs/cloud-messaging/admin/send-messages#send_to_individual_devices) * for code samples and detailed documentation. */ - interface MessagingDevicesResponse { - - /** - * The number of results that contain a canonical registration token. A - * canonical registration token is the registration token corresponding to the - * last registration requested by the client app. This is the token that you - * should use when sending future messages to the device. - * - * You can access the canonical registration tokens within the - * [`results`](#results) property. - */ + export interface MessagingDevicesResponse { canonicalRegistrationTokenCount: number; - - /** - * The number of messages that could not be processed and resulted in an error. - */ failureCount: number; - - /** - * The unique ID number identifying this multicast message. - */ multicastId: number; - - /** - * An array of `MessagingDeviceResult` objects representing the status of the - * processed messages. The objects are listed in the same order as in the - * request. That is, for each registration token in the request, its result has - * the same index in this array. If only a single registration token is - * provided, this array will contain a single object. - */ - results: admin.messaging.MessagingDeviceResult[]; - - /** - * The number of messages that were successfully processed and sent. - */ + results: MessagingDeviceResult[]; successCount: number; } + /** * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToDeviceGroup `sendToDeviceGroup()`} + * {@link messaging.Messaging.sendToDeviceGroup `sendToDeviceGroup()`} * method. * * See * [Send messages to device groups](/docs/cloud-messaging/send-message?authuser=0#send_messages_to_device_groups) * for code samples and detailed documentation. */ - interface MessagingDeviceGroupResponse { + export interface MessagingDeviceGroupResponse { /** * The number of messages that could not be processed and resulted in an error. @@ -1008,14 +1029,13 @@ export namespace admin.messaging { /** * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToTopic `sendToTopic()`} method. + * {@link messaging.Messaging.sendToTopic `sendToTopic()`} method. * * See * [Send to a topic](/docs/cloud-messaging/admin/send-messages#send_to_a_topic) * for code samples and detailed documentation. */ - interface MessagingTopicResponse { - + export interface MessagingTopicResponse { /** * The message ID for a successfully received request which FCM will attempt to * deliver to all subscribed devices. @@ -1025,14 +1045,13 @@ export namespace admin.messaging { /** * Interface representing the server response from the legacy - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendToCondition `sendToCondition()`} method. + * {@link messaging.Messaging.sendToCondition `sendToCondition()`} method. * * See * [Send to a condition](/docs/cloud-messaging/admin/send-messages#send_to_a_condition) * for code samples and detailed documentation. */ - interface MessagingConditionResponse { - + export interface MessagingConditionResponse { /** * The message ID for a successfully received request which FCM will attempt to * deliver to all subscribed devices. @@ -1042,18 +1061,15 @@ export namespace admin.messaging { /** * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#subscribeToTopic `subscribeToTopic()`} and - * {@link - * admin.messaging.Messaging#unsubscribeFromTopic - * `unsubscribeFromTopic()`} + * {@link messaging.Messaging.subscribeToTopic `subscribeToTopic()`} and + * {@link messaging.Messaging.unsubscribeFromTopic `unsubscribeFromTopic()`} * methods. * * See * [Manage topics from the server](/docs/cloud-messaging/manage-topics) * for code samples and detailed documentation. */ - interface MessagingTopicManagementResponse { - + export interface MessagingTopicManagementResponse { /** * The number of registration tokens that could not be subscribed to the topic * and resulted in an error. @@ -1070,20 +1086,20 @@ export namespace admin.messaging { * An array of errors corresponding to the provided registration token(s). The * length of this array will be equal to [`failureCount`](#failureCount). */ - errors: _admin.FirebaseArrayIndexError[]; + errors: FirebaseArrayIndexError[]; } /** * Interface representing the server response from the - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendAll `sendAll()`} and - * {@link https://firebase.google.com/docs/reference/admin/node/admin.messaging.Messaging#sendMulticast `sendMulticast()`} methods. + * {@link messaging.Messaging.sendAll `sendAll()`} and + * {@link messaging.Messaging.sendMulticast `sendMulticast()`} methods. */ - interface BatchResponse { + export interface BatchResponse { /** * An array of responses, each corresponding to a message. */ - responses: admin.messaging.SendResponse[]; + responses: SendResponse[]; /** * The number of messages that were successfully handed off for sending. @@ -1095,12 +1111,12 @@ export namespace admin.messaging { */ failureCount: number; } + /** * Interface representing the status of an individual message that was sent as * part of a batch request. */ - interface SendResponse { - + 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 @@ -1118,25 +1134,12 @@ export namespace admin.messaging { /** * An error, if the message was not handed off to FCM successfully. */ - error?: _admin.FirebaseError; + error?: FirebaseError; } - /** - * Gets the {@link admin.messaging.Messaging `Messaging`} service for the - * current app. - * - * @example - * ```javascript - * var messaging = app.messaging(); - * // The above is shorthand for: - * // var messaging = admin.messaging(app); - * ``` - * - * @return The `Messaging` service for the current app. - */ - interface Messaging { + export interface Messaging { /** - * The {@link admin.app.App app} associated with the current `Messaging` service + * The {@link app.App app} associated with the current `Messaging` service * instance. * * @example @@ -1144,7 +1147,7 @@ export namespace admin.messaging { * var app = messaging.app; * ``` */ - app: _admin.app.App; + app: app.App; /** * Sends the given message via FCM. @@ -1156,7 +1159,7 @@ export namespace admin.messaging { * string after the message has been successfully handed off to the FCM * service for delivery. */ - send(message: admin.messaging.Message, dryRun?: boolean): Promise; + send(message: Message, dryRun?: boolean): Promise; /** * Sends all the messages in the given array via Firebase Cloud Messaging. @@ -1178,9 +1181,9 @@ export namespace admin.messaging { * send operation. */ sendAll( - messages: Array, + messages: Array, dryRun?: boolean - ): Promise; + ): Promise; /** * Sends the given multicast message to all the FCM registration tokens @@ -1201,9 +1204,9 @@ export namespace admin.messaging { * send operation. */ sendMulticast( - message: admin.messaging.MulticastMessage, + message: MulticastMessage, dryRun?: boolean - ): Promise; + ): Promise; /** * Sends an FCM message to a single device corresponding to the provided @@ -1227,9 +1230,9 @@ export namespace admin.messaging { */ sendToDevice( registrationToken: string | string[], - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; /** * Sends an FCM message to a device group corresponding to the provided @@ -1250,9 +1253,9 @@ export namespace admin.messaging { */ sendToDeviceGroup( notificationKey: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; /** * Sends an FCM message to a topic. @@ -1271,9 +1274,9 @@ export namespace admin.messaging { */ sendToTopic( topic: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; /** * Sends an FCM message to a condition. @@ -1293,9 +1296,9 @@ export namespace admin.messaging { */ sendToCondition( condition: string, - payload: admin.messaging.MessagingPayload, - options?: admin.messaging.MessagingOptions - ): Promise; + payload: MessagingPayload, + options?: MessagingOptions + ): Promise; /** * Subscribes a device to an FCM topic. @@ -1315,7 +1318,7 @@ export namespace admin.messaging { subscribeToTopic( registrationTokens: string | string[], topic: string - ): Promise; + ): Promise; /** * Unsubscribes a device from an FCM topic. @@ -1335,6 +1338,6 @@ export namespace admin.messaging { unsubscribeFromTopic( registrationTokens: string | string[], topic: string - ): Promise; + ): Promise; } } diff --git a/src/messaging/messaging-api-request.ts b/src/messaging/messaging-api-request-internal.ts similarity index 90% rename from src/messaging/messaging-api-request.ts rename to src/messaging/messaging-api-request-internal.ts index 9af1ff8571..d74f621e0d 100644 --- a/src/messaging/messaging-api-request.ts +++ b/src/messaging/messaging-api-request-internal.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,23 +15,27 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; +import { FirebaseApp } from '../firebase-app'; import { HttpMethod, AuthorizedHttpClient, HttpRequestConfig, HttpError, HttpResponse, } from '../utils/api-request'; -import { createFirebaseError, getErrorCode } from './messaging-errors'; -import { SubRequest, BatchRequestClient } from './batch-request'; -import { SendResponse, BatchResponse } from './messaging-types'; +import { createFirebaseError, getErrorCode } from './messaging-errors-internal'; +import { SubRequest, BatchRequestClient } from './batch-request-internal'; +import { messaging } from './index'; +import { getSdkVersion } from '../utils/index'; + +import SendResponse = messaging.SendResponse; +import BatchResponse = messaging.BatchResponse; // FCM backend constants const FIREBASE_MESSAGING_TIMEOUT = 10000; const FIREBASE_MESSAGING_BATCH_URL = 'https://fcm.googleapis.com/batch'; const FIREBASE_MESSAGING_HTTP_METHOD: HttpMethod = 'POST'; const FIREBASE_MESSAGING_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; const LEGACY_FIREBASE_MESSAGING_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'access_token_auth': 'true', }; diff --git a/src/messaging/messaging-errors.ts b/src/messaging/messaging-errors-internal.ts similarity index 96% rename from src/messaging/messaging-errors.ts rename to src/messaging/messaging-errors-internal.ts index b317404087..ecd3155bae 100644 --- a/src/messaging/messaging-errors.ts +++ b/src/messaging/messaging-errors-internal.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import {HttpError} from '../utils/api-request'; -import {FirebaseMessagingError, MessagingClientErrorCode} from '../utils/error'; +import { HttpError } from '../utils/api-request'; +import { FirebaseMessagingError, MessagingClientErrorCode } from '../utils/error'; import * as validator from '../utils/validator'; /** diff --git a/src/messaging/messaging-types.ts b/src/messaging/messaging-internal.ts similarity index 72% rename from src/messaging/messaging-types.ts rename to src/messaging/messaging-internal.ts index 27891d27ed..d84328e722 100644 --- a/src/messaging/messaging-types.ts +++ b/src/messaging/messaging-internal.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019 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,281 +14,33 @@ * limitations under the License. */ -import {renameProperties} from '../utils/index'; -import { - MessagingClientErrorCode, FirebaseMessagingError, FirebaseArrayIndexError, FirebaseError, -} from '../utils/error'; - +import { renameProperties, transformMillisecondsToSecondsString } from '../utils/index'; +import { MessagingClientErrorCode, FirebaseMessagingError, } from '../utils/error'; +import { messaging } from './index'; import * as validator from '../utils/validator'; -interface BaseMessage { - data?: {[key: string]: string}; - notification?: Notification; - android?: AndroidConfig; - webpush?: WebpushConfig; - apns?: ApnsConfig; - fcmOptions?: FcmOptions; -} - -interface TokenMessage extends BaseMessage { - token: string; -} - -interface TopicMessage extends BaseMessage { - topic: string; -} - -interface ConditionMessage extends BaseMessage { - condition: string; -} - -/** - * Payload for the admin.messaging.send() operation. The payload contains all the fields - * in the BaseMessage type, and exactly one of token, topic or condition. - */ -export type Message = TokenMessage | TopicMessage | ConditionMessage; - -/** - * Payload for the admin.messaing.sendMulticase() method. The payload contains all the fields - * in the BaseMessage type, and a list of tokens. - */ -export interface MulticastMessage extends BaseMessage { - tokens: string[]; -} - -export interface Notification { - title?: string; - body?: string; - imageUrl?: string; -} - -export interface FcmOptions { - analyticsLabel?: string; -} - -export interface WebpushConfig { - headers?: {[key: string]: string}; - data?: {[key: string]: string}; - notification?: WebpushNotification; - fcmOptions?: WebpushFcmOptions; -} - -export interface WebpushFcmOptions { - link?: string; -} - -export interface WebpushNotification { - title?: string; - 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; - vibrate?: number | number[]; - [key: string]: any; -} - -export interface ApnsConfig { - headers?: {[key: string]: string}; - payload?: ApnsPayload; - fcmOptions?: ApnsFcmOptions; -} - -export interface ApnsPayload { - aps: Aps; - [customData: string]: object; -} - -export interface Aps { - alert?: string | ApsAlert; - badge?: number; - sound?: string | CriticalSound; - contentAvailable?: boolean; - category?: string; - threadId?: string; - mutableContent?: boolean; - [customData: string]: any; -} - -export interface CriticalSound { - critical?: boolean; - name: string; - volume?: number; -} - -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; -} - -export interface ApnsFcmOptions { - analyticsLabel?: string; - imageUrl?: string; -} - -export interface AndroidConfig { - collapseKey?: string; - priority?: ('high' | 'normal'); - ttl?: number; - restrictedPackageName?: string; - data?: {[key: string]: string}; - notification?: AndroidNotification; - fcmOptions?: AndroidFcmOptions; -} - -export interface AndroidNotification { - title?: string; - body?: string; - icon?: string; - color?: string; - sound?: string; - tag?: string; - imageUrl?: string; - clickAction?: string; - bodyLocKey?: string; - bodyLocArgs?: string[]; - titleLocKey?: string; - titleLocArgs?: string[]; - channelId?: string; - ticker?: string; - sticky?: boolean; - eventTimestamp?: Date; - localOnly?: boolean; - priority?: ('min' | 'low' | 'default' | 'high' | 'max'); - vibrateTimingsMillis?: number[]; - defaultVibrateTimings?: boolean; - defaultSound?: boolean; - lightSettings?: LightSettings; - defaultLightSettings?: boolean; - visibility?: ('private' | 'public' | 'secret'); - notificationCount?: number; -} - -export interface LightSettings { - color: string; - lightOnDurationMillis: number; - lightOffDurationMillis: number; -} - -export interface AndroidFcmOptions { - analyticsLabel?: string; -} - -/* 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 | undefined; -} - -/* 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[]; -} - -/* Response from sending a batch of messages. */ -export interface BatchResponse { - responses: SendResponse[]; - failureCount: number; - successCount: number; -} - -/* The result of a sub request sent in a batch. */ -export interface SendResponse { - success: boolean; - messageId?: string; - error?: FirebaseError; -} +import AndroidConfig = messaging.AndroidConfig; +import AndroidFcmOptions = messaging.AndroidFcmOptions; +import AndroidNotification = messaging.AndroidNotification; +import ApsAlert = messaging.ApsAlert; +import ApnsConfig = messaging.ApnsConfig; +import ApnsFcmOptions = messaging.ApnsFcmOptions; +import ApnsPayload = messaging.ApnsPayload; +import Aps = messaging.Aps; +import CriticalSound = messaging.CriticalSound; +import FcmOptions = messaging.FcmOptions; +import LightSettings = messaging.LightSettings; +import Message = messaging.Message; +import Notification = messaging.Notification; +import WebpushConfig = messaging.WebpushConfig; + +// Keys which are not allowed in the messaging data payload object. +export const BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; + +// 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 @@ -337,7 +89,7 @@ export function validateMessage(message: Message): void { * @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 { +function validateStringMap(map: { [key: string]: any } | undefined, label: string): void { if (typeof map === 'undefined') { return; } else if (!validator.isNonNullObject(map)) { @@ -400,7 +152,7 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { } if (typeof fcmOptions.imageUrl !== 'undefined' && - !validator.isURL(fcmOptions.imageUrl)) { + !validator.isURL(fcmOptions.imageUrl)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, 'imageUrl must be a valid URL string'); @@ -411,7 +163,7 @@ function validateApnsFcmOptions(fcmOptions: ApnsFcmOptions | undefined): void { MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } - const propertyMappings: {[key: string]: string} = { + const propertyMappings: { [key: string]: string } = { imageUrl: 'image', }; Object.keys(propertyMappings).forEach((key) => { @@ -461,7 +213,7 @@ function validateNotification(notification: Notification | undefined): void { MessagingClientErrorCode.INVALID_PAYLOAD, 'notification.imageUrl must be a valid URL string'); } - const propertyMappings: {[key: string]: string} = { + const propertyMappings: { [key: string]: string } = { imageUrl: 'image', }; Object.keys(propertyMappings).forEach((key) => { @@ -505,7 +257,7 @@ function validateAps(aps: Aps): void { validateApsAlert(aps.alert); validateApsSound(aps.sound); - const propertyMappings: {[key: string]: string} = { + const propertyMappings: { [key: string]: string } = { contentAvailable: 'content-available', mutableContent: 'mutable-content', threadId: 'thread-id', @@ -564,7 +316,7 @@ function validateApsSound(sound: string | CriticalSound | undefined): void { 'apns.payload.aps.sound.volume must be in the interval [0, 1]'); } } - const soundObject = sound as {[key: string]: any}; + const soundObject = sound as { [key: string]: any }; const key = 'critical'; const critical = soundObject[key]; if (typeof critical !== 'undefined' && critical !== 1) { @@ -594,19 +346,19 @@ function validateApsAlert(alert: string | ApsAlert | undefined): void { const apsAlert: ApsAlert = alert as ApsAlert; if (validator.isNonEmptyArray(apsAlert.locArgs) && - !validator.isNonEmptyString(apsAlert.locKey)) { + !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)) { + !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)) { + !validator.isNonEmptyString(apsAlert.subtitleLocKey)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, 'apns.payload.aps.alert.subtitleLocKey is required when specifying subtitleLocArgs'); @@ -680,19 +432,19 @@ function validateAndroidNotification(notification: AndroidNotification | undefin MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.color must be in the form #RRGGBB'); } if (validator.isNonEmptyArray(notification.bodyLocArgs) && - !validator.isNonEmptyString(notification.bodyLocKey)) { + !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)) { + !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)) { + !validator.isURL(notification.imageUrl)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, 'android.notification.imageUrl must be a valid URL string'); @@ -837,28 +589,3 @@ function validateAndroidFcmOptions(fcmOptions: AndroidFcmOptions | undefined): v MessagingClientErrorCode.INVALID_PAYLOAD, 'analyticsLabel must be a string value'); } } - -/** - * Transforms milliseconds to the format expected by FCM service. - * Returns the duration in seconds with up to nine fractional - * digits, terminated by 's'. Example: "3.5s". - * - * @param {number} milliseconds The duration in milliseconds. - * @return {string} The resulting formatted string in seconds with up to nine fractional - * digits, terminated by 's'. - */ -function transformMillisecondsToSecondsString(milliseconds: number): string { - let duration: string; - const seconds = Math.floor(milliseconds / 1000); - const nanos = (milliseconds - seconds * 1000) * 1000000; - if (nanos > 0) { - let nanoString = nanos.toString(); - while (nanoString.length < 9) { - nanoString = '0' + nanoString; - } - duration = `${seconds}.${nanoString}s`; - } else { - duration = `${seconds}s`; - } - return duration; -} diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index 6cabb1104b..88e66cf565 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,24 +15,32 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {deepCopy, deepExtend} from '../utils/deep-copy'; -import {SubRequest} from './batch-request'; -import { - Message, validateMessage, MessagingDevicesResponse, - MessagingDeviceGroupResponse, MessagingTopicManagementResponse, - MessagingPayload, MessagingOptions, MessagingTopicResponse, - MessagingConditionResponse, BatchResponse, MulticastMessage, DataMessagePayload, NotificationMessagePayload, -} from './messaging-types'; -import {FirebaseMessagingRequestHandler} from './messaging-api-request'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import { - ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError, -} from '../utils/error'; - +import { FirebaseApp } from '../firebase-app'; +import { deepCopy, deepExtend } from '../utils/deep-copy'; +import { SubRequest } from './batch-request-internal'; +import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal'; +import { messaging } from './index'; +import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal'; +import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error'; import * as utils from '../utils'; import * as validator from '../utils/validator'; +import MessagingInterface = messaging.Messaging; +import Message = messaging.Message; +import BatchResponse = messaging.BatchResponse; +import MulticastMessage = messaging.MulticastMessage; +import MessagingTopicManagementResponse = messaging.MessagingTopicManagementResponse; + +// Legacy API types +import MessagingDevicesResponse = messaging.MessagingDevicesResponse; +import MessagingDeviceGroupResponse = messaging.MessagingDeviceGroupResponse; +import MessagingPayload = messaging.MessagingPayload; +import MessagingOptions = messaging.MessagingOptions; +import MessagingTopicResponse = messaging.MessagingTopicResponse; +import MessagingConditionResponse = messaging.MessagingConditionResponse; +import DataMessagePayload = messaging.DataMessagePayload; +import NotificationMessagePayload = messaging.NotificationMessagePayload; + /* eslint-disable @typescript-eslint/camelcase */ // FCM endpoints @@ -94,14 +103,6 @@ const MESSAGING_CONDITION_RESPONSE_KEYS_MAP = { message_id: 'messageId', }; -// Keys which are not allowed in the messaging data payload object. -export const BLACKLISTED_DATA_PAYLOAD_KEYS = ['from']; - -// Keys which are not allowed in the messaging options object. -export const BLACKLISTED_OPTIONS_KEYS = [ - 'condition', 'data', 'notification', 'registrationIds', 'registration_ids', 'to', -]; - /** * Maps a raw FCM server response to a MessagingDevicesResponse object. * @@ -184,36 +185,27 @@ 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 implements MessagingInterface { private urlPath: string; private readonly appInternal: FirebaseApp; private readonly messagingRequestHandler: FirebaseMessagingRequestHandler; /** - * @param {FirebaseApp} app The app for this Messaging service. - * @constructor + * Gets the {@link messaging.Messaging `Messaging`} service for the + * current app. + * + * @example + * ```javascript + * var messaging = app.messaging(); + * // The above is shorthand for: + * // var messaging = admin.messaging(app); + * ``` + * + * @return The `Messaging` service for the current app. */ constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { @@ -237,12 +229,14 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends a message via Firebase Cloud Messaging (FCM). - * - * @param {Message} message The message to be sent. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. + * Sends the given message via FCM. * - * @return {Promise} A Promise fulfilled with a message ID string. + * @param message The message payload. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A promise fulfilled with a unique message ID + * string after the message has been successfully handed off to the FCM + * service for delivery. */ public send(message: Message, dryRun?: boolean): Promise { const copy: Message = deepCopy(message); @@ -253,7 +247,7 @@ export class Messaging implements FirebaseServiceInterface { } return this.getUrlPath() .then((urlPath) => { - const request: {message: Message; validate_only?: boolean} = {message: copy}; + const request: { message: Message; validate_only?: boolean } = { message: copy }; if (dryRun) { request.validate_only = true; } @@ -265,19 +259,23 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends all the messages in the given array via Firebase Cloud Messaging. Employs batching to - * send the entire list as a single RPC call. Compared to the send() method, this method is a - * significantly more efficient way to send multiple messages. + * Sends all the messages in the given array via Firebase Cloud Messaging. + * Employs batching to send the entire list as a single RPC call. Compared + * to the `send()` method, this method is a significantly more efficient way + * to send multiple messages. * - * The responses list obtained from the return value corresponds to the order of input messages. - * An error from this method indicates a total failure -- i.e. none of the messages in the - * list could be sent. Partial failures are indicated by a BatchResponse return value. + * The responses list obtained from the return value + * corresponds to the order of tokens in the `MulticastMessage`. An error + * from this method indicates a total failure -- i.e. none of the messages in + * the list could be sent. Partial failures are indicated by a `BatchResponse` + * return value. * - * @param {Message[]} messages A non-empty array containing up to 500 messages. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. - * - * @return {Promise} A Promise fulfilled with an object representing the result - * of the send operation. + * @param messages A non-empty array + * containing up to 500 messages. + * @param dryRun Whether to send the messages in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. */ public sendAll(messages: Message[], dryRun?: boolean): Promise { if (validator.isArray(messages) && messages.constructor !== Array) { @@ -306,7 +304,7 @@ export class Messaging implements FirebaseServiceInterface { .then((urlPath) => { const requests: SubRequest[] = copy.map((message) => { validateMessage(message); - const request: {message: Message; validate_only?: boolean} = {message}; + const request: { message: Message; validate_only?: boolean } = { message }; if (dryRun) { request.validate_only = true; } @@ -320,19 +318,22 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends the given multicast message to all the FCM registration tokens specified in it. - * - * This method uses the sendAll() API under the hood to send the given - * message to all the target recipients. The responses list obtained from the return value - * corresponds to the order of tokens in the MulticastMessage. An error from this method - * indicates a total failure -- i.e. none of the tokens in the list could be sent to. Partial - * failures are indicated by a BatchResponse return value. + * Sends the given multicast message to all the FCM registration tokens + * specified in it. * - * @param {MulticastMessage} message A multicast message containing up to 500 tokens. - * @param {boolean=} dryRun Whether to send the message in the dry-run (validation only) mode. + * This method uses the `sendAll()` API under the hood to send the given + * message to all the target recipients. The responses list obtained from the + * return value corresponds to the order of tokens in the `MulticastMessage`. + * An error from this method indicates a total failure -- i.e. the message was + * not sent to any of the tokens in the list. Partial failures are indicated by + * a `BatchResponse` return value. * - * @return {Promise} A Promise fulfilled with an object representing the result - * of the send operation. + * @param message A multicast message + * containing up to 500 tokens. + * @param dryRun Whether to send the message in the dry-run + * (validation only) mode. + * @return A Promise fulfilled with an object representing the result of the + * send operation. */ public sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise { const copy: MulticastMessage = deepCopy(message); @@ -365,21 +366,30 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Sends an FCM message to a single device or an array of devices. + * Sends an FCM message to a single device corresponding to the provided + * registration token. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens for the device(s) to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to individual devices](/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices) + * for code samples and detailed documentation. Takes either a + * `registrationToken` to send to a single device or a + * `registrationTokens` parameter containing an array of tokens to send + * to multiple devices. + * + * @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. + * @return A promise fulfilled with the server's response after the message + * has been sent. */ 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( @@ -417,27 +427,39 @@ 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. * - * @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. + * See + * [Send to a device group](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group) + * for code samples and detailed documentation. * - * @return {Promise} A Promise fulfilled - * with the server's response after the message has been sent. + * @param notificationKey The notification key for the device group to + * which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToDeviceGroup( notificationKey: string, payload: MessagingPayload, options: MessagingOptions = {}, - ): Promise { + ): Promise { if (!validator.isNonEmptyString(notificationKey)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_RECIPIENT, @@ -489,7 +511,11 @@ export class Messaging implements FirebaseServiceInterface { 'Notification key provided to sendToDeviceGroup() is invalid.', ); } else { - return mapRawResponseToDevicesResponse(response); + const devicesResponse = mapRawResponseToDevicesResponse(response); + return { + ...devicesResponse, + failedRegistrationTokens: [], + } } } @@ -500,12 +526,17 @@ export class Messaging implements FirebaseServiceInterface { /** * Sends an FCM message to a topic. * - * @param {string} topic The name of the topic to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to a topic](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic) + * for code samples and detailed documentation. + * + * @param topic The topic to which to send the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. * - * @return {Promise} A Promise fulfilled with the server's response after - * the message has been sent. + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToTopic( topic: string, @@ -545,12 +576,18 @@ export class Messaging implements FirebaseServiceInterface { /** * Sends an FCM message to a condition. * - * @param {string} condition The condition to which to send the message. - * @param {MessagingPayload} payload The message payload. - * @param {MessagingOptions} [options = {}] Optional options to alter the message. + * See + * [Send to a condition](/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition) + * for code samples and detailed documentation. * - * @return {Promise} A Promise fulfilled with the server's response - * after the message has been sent. + * @param condition The condition determining to which topics to send + * the message. + * @param payload The message payload. + * @param options Optional options to + * alter the message. + * + * @return A promise fulfilled with the server's response after the message + * has been sent. */ public sendToCondition( condition: string, @@ -595,14 +632,19 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Subscribes a single device or an array of devices to a topic. + * Subscribes a device to an FCM topic. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens to subscribe to the topic. - * @param {string} topic The topic to which to subscribe. + * See [Subscribe to a + * topic](/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to subscribe multiple devices. + * + * @param registrationTokens A token or array of registration tokens + * for the devices to subscribe to the topic. + * @param topic The topic to which to subscribe. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @return A promise fulfilled with the server's response after the device has been + * subscribed to the topic. */ public subscribeToTopic( registrationTokenOrTokens: string | string[], @@ -617,14 +659,19 @@ export class Messaging implements FirebaseServiceInterface { } /** - * Unsubscribes a single device or an array of devices from a topic. + * Unsubscribes a device from an FCM topic. * - * @param {string|string[]} registrationTokenOrTokens The registration token or an array of - * registration tokens to unsubscribe from the topic. - * @param {string} topic The topic to which to subscribe. + * See [Unsubscribe from a + * topic](/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic) + * for code samples and detailed documentation. Optionally, you can provide an + * array of tokens to unsubscribe multiple devices. + * + * @param registrationTokens A device registration token or an array of + * device registration tokens to unsubscribe from the topic. + * @param topic The topic from which to unsubscribe. * - * @return {Promise} A Promise fulfilled with the parsed FCM - * server response. + * @return A promise fulfilled with the server's response after the device has been + * unsubscribed from the topic. */ public unsubscribeFromTopic( registrationTokenOrTokens: string | string[], @@ -650,8 +697,8 @@ export class Messaging implements FirebaseServiceInterface { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_ARGUMENT, 'Failed to determine project ID for Messaging. Initialize the ' - + 'SDK with service account credentials or set project ID as an app option. ' - + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.', ); } @@ -758,8 +805,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; @@ -779,8 +826,8 @@ export class Messaging implements FirebaseServiceInterface { if (!validator.isNonNullObject(value)) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid value for the "${ payloadKey }" property. ` + - `Value must be an object.`, + `Messaging payload contains an invalid value for the "${payloadKey}" property. ` + + 'Value must be an object.', ); } @@ -789,14 +836,14 @@ export class Messaging implements FirebaseServiceInterface { // Validate all sub-keys have a string value throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains an invalid value for the "${ payloadKey }.${ subKey }" ` + - `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.`, ); } }); @@ -815,7 +862,7 @@ export class Messaging implements FirebaseServiceInterface { if (blacklistedKey in payloadCopy.data!) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_PAYLOAD, - `Messaging payload contains the blacklisted "data.${ blacklistedKey }" property.`, + `Messaging payload contains the blacklisted "data.${blacklistedKey}" property.`, ); } }); @@ -845,7 +892,7 @@ export class Messaging implements FirebaseServiceInterface { if (blacklistedKey in optionsCopy) { throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains the blacklisted "${ blacklistedKey }" property.`, + `Messaging options contains the blacklisted "${blacklistedKey}" property.`, ); } }); @@ -858,14 +905,14 @@ export class Messaging implements FirebaseServiceInterface { const keyName = ('collapseKey' in options) ? 'collapseKey' : 'collapse_key'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a non-empty string.', ); } else if ('dry_run' in optionsCopy && !validator.isBoolean((optionsCopy as any).dry_run)) { const keyName = ('dryRun' in options) ? 'dryRun' : 'dry_run'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } else if ('priority' in optionsCopy && !validator.isNonEmptyString(optionsCopy.priority)) { @@ -875,32 +922,32 @@ export class Messaging implements FirebaseServiceInterface { 'be a non-empty string.', ); } else if ('restricted_package_name' in optionsCopy && - !validator.isNonEmptyString((optionsCopy as any).restricted_package_name)) { + !validator.isNonEmptyString((optionsCopy as any).restricted_package_name)) { const keyName = ('restrictedPackageName' in options) ? 'restrictedPackageName' : 'restricted_package_name'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a non-empty string.', ); } else if ('time_to_live' in optionsCopy && !validator.isNumber((optionsCopy as any).time_to_live)) { const keyName = ('timeToLive' in options) ? 'timeToLive' : 'time_to_live'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a number.', ); } else if ('content_available' in optionsCopy && !validator.isBoolean((optionsCopy as any).content_available)) { const keyName = ('contentAvailable' in options) ? 'contentAvailable' : 'content_available'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } else if ('mutable_content' in optionsCopy && !validator.isBoolean((optionsCopy as any).mutable_content)) { const keyName = ('mutableContent' in options) ? 'mutableContent' : 'mutable_content'; throw new FirebaseMessagingError( MessagingClientErrorCode.INVALID_OPTIONS, - `Messaging options contains an invalid value for the "${ keyName }" property. Value must ` + + `Messaging options contains an invalid value for the "${keyName}" property. Value must ` + 'be a boolean.', ); } @@ -921,7 +968,7 @@ export class Messaging implements FirebaseServiceInterface { errorInfo: ErrorInfo = MessagingClientErrorCode.INVALID_ARGUMENT, ): void { if (!validator.isNonEmptyArray(registrationTokenOrTokens) && - !validator.isNonEmptyString(registrationTokenOrTokens)) { + !validator.isNonEmptyString(registrationTokenOrTokens)) { throw new FirebaseMessagingError( errorInfo, `Registration token(s) provided to ${methodName}() must be a non-empty string or a ` + @@ -1017,7 +1064,7 @@ export class Messaging implements FirebaseServiceInterface { */ private normalizeTopic(topic: string): string { if (!/^\/topics\//.test(topic)) { - topic = `/topics/${ topic }`; + topic = `/topics/${topic}`; } return topic; } diff --git a/src/project-management/android-app.ts b/src/project-management/android-app.ts index 0cbd8951fc..8afba57094 100644 --- a/src/project-management/android-app.ts +++ b/src/project-management/android-app.ts @@ -16,10 +16,15 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; -import { AndroidAppMetadata, AppPlatform } from './app-metadata'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; +import { projectManagement } from './index'; -export class AndroidApp { +import AndroidAppInterface = projectManagement.AndroidApp; +import AndroidAppMetadata = projectManagement.AndroidAppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import ShaCertificateInterface = projectManagement.ShaCertificate; + +export class AndroidApp implements AndroidAppInterface { private readonly resourceName: string; constructor( @@ -33,6 +38,11 @@ export class AndroidApp { this.resourceName = `projects/-/androidApps/${appId}`; } + /** + * Retrieves metadata about this Android app. + * + * @return A promise that resolves to the retrieved metadata about this Android app. + */ public getMetadata(): Promise { return this.requestHandler.getResource(this.resourceName) .then((responseData: any) => { @@ -61,10 +71,23 @@ export class AndroidApp { }); } + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has been set. + */ public setDisplayName(newDisplayName: string): Promise { return this.requestHandler.setDisplayName(this.resourceName, newDisplayName); } + /** + * Gets the list of SHA certificates associated with this Android app in Firebase. + * + * @return The list of SHA-1 and SHA-256 certificates associated with this Android app in + * Firebase. + */ public getShaCertificates(): Promise { return this.requestHandler.getAndroidShaCertificates(this.resourceName) .then((responseData: any) => { @@ -90,7 +113,7 @@ export class AndroidApp { validator.isNonEmptyString(certificateJson[requiredField]), responseData, `getShaCertificates()'s responseData.certificates[].${requiredField} must be a ` - + `non-empty string.`); + + 'non-empty string.'); }); return new ShaCertificate(certificateJson.shaHash, certificateJson.name); @@ -98,10 +121,26 @@ export class AndroidApp { }); } + /** + * Adds the given SHA certificate to this Android app. + * + * @param certificateToAdd The SHA certificate to add. + * + * @return A promise that resolves when the given certificate + * has been added to the Android app. + */ public addShaCertificate(certificateToAdd: ShaCertificate): Promise { return this.requestHandler.addAndroidShaCertificate(this.resourceName, certificateToAdd); } + /** + * Deletes the specified SHA certificate from this Android app. + * + * @param certificateToDelete The SHA certificate to delete. + * + * @return A promise that resolves when the specified + * certificate has been removed from the Android app. + */ public deleteShaCertificate(certificateToDelete: ShaCertificate): Promise { if (!certificateToDelete.resourceName) { throw new FirebaseProjectManagementError( @@ -113,8 +152,12 @@ export class AndroidApp { } /** - * @return {Promise} A promise that resolves to a UTF-8 JSON string, typically intended to - * be written to a JSON file. + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the Android app's + * Firebase config file, in UTF-8 string format. This string is typically + * intended to be written to a JSON file that gets shipped with your Android + * app. */ public getConfig(): Promise { return this.requestHandler.getConfig(this.resourceName) @@ -128,14 +171,28 @@ export class AndroidApp { assertServerResponse( validator.isBase64String(base64ConfigFileContents), responseData, - `getConfig()'s responseData.configFileContents must be a base64 string.`); + 'getConfig()\'s responseData.configFileContents must be a base64 string.'); return Buffer.from(base64ConfigFileContents, 'base64').toString('utf8'); }); } } -export class ShaCertificate { +/** + * A SHA-1 or SHA-256 certificate. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.shaCertificate()`](projectManagement.ProjectManagement#shaCertificate). + */ +export class ShaCertificate implements ShaCertificateInterface { + /** + * The SHA certificate type. + * + * @example + * ```javascript + * var certType = shaCertificate.certType; + * ``` + */ public readonly certType: ('sha1' | 'sha256'); /** @@ -143,8 +200,16 @@ export class ShaCertificate { * automatically determined from the hash itself. * * @param shaHash The sha256 or sha1 hash for this certificate. + * @example + * ```javascript + * var shaHash = shaCertificate.shaHash; + * ``` * @param resourceName The Firebase resource name for this certificate. This does not need to be * set when creating a new certificate. + * @example + * ```javascript + * var resourceName = shaCertificate.resourceName; + * ``` */ constructor(public readonly shaHash: string, public readonly resourceName?: string) { if (/^[a-fA-F0-9]{40}$/.test(shaHash)) { diff --git a/src/project-management.d.ts b/src/project-management/index.ts similarity index 73% rename from src/project-management.d.ts rename to src/project-management/index.ts index 755f8c9e94..2c905d6c22 100644 --- a/src/project-management.d.ts +++ b/src/project-management/index.ts @@ -1,53 +1,76 @@ -import * as _admin from './index.d'; - -export namespace admin.projectManagement { - +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link projectManagement.ProjectManagement + * `ProjectManagement`} service for the default app or a given app. + * + * `admin.projectManagement()` can be called with no arguments to access the + * default app's {@link projectManagement.ProjectManagement + * `ProjectManagement`} service, or as `admin.projectManagement(app)` to access + * the {@link projectManagement.ProjectManagement `ProjectManagement`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the ProjectManagement service for the default app + * var defaultProjectManagement = admin.projectManagement(); + * ``` + * + * @example + * ```javascript + * // Get the ProjectManagement service for a given app + * var otherProjectManagement = admin.projectManagement(otherApp); + * ``` + * + * @param app Optional app whose `ProjectManagement` service + * to return. If not provided, the default `ProjectManagement` service will + * be returned. * + * @return The default `ProjectManagement` service if no app is provided or the + * `ProjectManagement` service associated with the provided app. + */ +export declare function projectManagement(app?: app.App): projectManagement.ProjectManagement; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace projectManagement { /** - * A SHA-1 or SHA-256 certificate. - * - * Do not call this constructor directly. Instead, use - * [`projectManagement.shaCertificate()`](admin.projectManagement.ProjectManagement#shaCertificate). + * Metadata about a Firebase Android App. */ - interface ShaCertificate { - - /** - * The SHA certificate type. - * - * @example - * ```javascript - * var certType = shaCertificate.certType; - * ``` - */ - certType: ('sha1' | 'sha256'); + export interface AndroidAppMetadata extends AppMetadata { - /** - * The SHA-1 or SHA-256 hash for this certificate. - * - * @example - * ```javascript - * var shaHash = shaCertificate.shaHash; - * ``` - */ - shaHash: string; + platform: AppPlatform.ANDROID; /** - * The fully-qualified resource name that identifies this sha-key. - * - * This is useful when manually constructing requests for Firebase's public API. + * The canonical package name of the Android App, as would appear in the Google Play Developer + * Console. * * @example * ```javascript - * var resourceName = shaCertificate.resourceName; + * var packageName = androidAppMetadata.packageName; * ``` */ - resourceName?: string; + packageName: string; } /** * Metadata about a Firebase app. */ - interface AppMetadata { - + export interface AppMetadata { /** * The globally unique, Firebase-assigned identifier of the app. * @@ -104,8 +127,7 @@ export namespace admin.projectManagement { /** * Platforms with which a Firebase App can be associated. */ - enum AppPlatform { - + export enum AppPlatform { /** * Unknown state. This is only used for distinguishing unset values. */ @@ -122,29 +144,10 @@ export namespace admin.projectManagement { ANDROID = 'ANDROID', } - /** - * Metadata about a Firebase Android App. - */ - interface AndroidAppMetadata extends AppMetadata { - - platform: AppPlatform.ANDROID; - - /** - * The canonical package name of the Android App, as would appear in the Google Play Developer - * Console. - * - * @example - * ```javascript - * var packageName = androidAppMetadata.packageName; - * ``` - */ - packageName: string; - } - /** * Metadata about a Firebase iOS App. */ - interface IosAppMetadata extends AppMetadata { + export interface IosAppMetadata extends AppMetadata { platform: AppPlatform.IOS; /** @@ -162,9 +165,9 @@ export namespace admin.projectManagement { * A reference to a Firebase Android app. * * Do not call this constructor directly. Instead, use - * [`projectManagement.androidApp()`](admin.projectManagement.ProjectManagement#androidApp). + * [`projectManagement.androidApp()`](projectManagement.ProjectManagement#androidApp). */ - interface AndroidApp { + export interface AndroidApp { appId: string; /** @@ -172,7 +175,7 @@ export namespace admin.projectManagement { * * @return A promise that resolves to the retrieved metadata about this Android app. */ - getMetadata(): Promise; + getMetadata(): Promise; /** * Sets the optional user-assigned display name of the app. @@ -189,7 +192,7 @@ export namespace admin.projectManagement { * @return The list of SHA-1 and SHA-256 certificates associated with this Android app in * Firebase. */ - getShaCertificates(): Promise; + getShaCertificates(): Promise; /** * Adds the given SHA certificate to this Android app. @@ -226,9 +229,9 @@ export namespace admin.projectManagement { * A reference to a Firebase iOS app. * * Do not call this constructor directly. Instead, use - * [`projectManagement.iosApp()`](admin.projectManagement.ProjectManagement#iosApp). + * [`projectManagement.iosApp()`](projectManagement.ProjectManagement#iosApp). */ - interface IosApp { + export interface IosApp { appId: string; /** @@ -237,7 +240,7 @@ export namespace admin.projectManagement { * @return {!Promise} A promise that * resolves to the retrieved metadata about this iOS app. */ - getMetadata(): Promise; + getMetadata(): Promise; /** * Sets the optional user-assigned display name of the app. @@ -259,35 +262,76 @@ export namespace admin.projectManagement { getConfig(): Promise; } + /** + * A SHA-1 or SHA-256 certificate. + * + * Do not call this constructor directly. Instead, use + * [`projectManagement.shaCertificate()`](projectManagement.ProjectManagement#shaCertificate). + */ + export interface ShaCertificate { + + /** + * The SHA certificate type. + * + * @example + * ```javascript + * var certType = shaCertificate.certType; + * ``` + */ + certType: ('sha1' | 'sha256'); + + /** + * The SHA-1 or SHA-256 hash for this certificate. + * + * @example + * ```javascript + * var shaHash = shaCertificate.shaHash; + * ``` + */ + shaHash: string; + + /** + * The fully-qualified resource name that identifies this sha-key. + * + * This is useful when manually constructing requests for Firebase's public API. + * + * @example + * ```javascript + * var resourceName = shaCertificate.resourceName; + * ``` + */ + resourceName?: string; + } + /** * The Firebase ProjectManagement service interface. * * Do not call this constructor directly. Instead, use - * [`admin.projectManagement()`](admin.projectManagement#projectManagement). + * [`admin.projectManagement()`](projectManagement#projectManagement). */ - interface ProjectManagement { - app: _admin.app.App; + export interface ProjectManagement { + app: app.App; /** * Lists up to 100 Firebase apps associated with this Firebase project. * * @return A promise that resolves to the metadata list of the apps. */ - listAppMetadata(): Promise; + listAppMetadata(): Promise; /** * Lists up to 100 Firebase Android apps associated with this Firebase project. * * @return The list of Android apps. */ - listAndroidApps(): Promise; + listAndroidApps(): Promise; /** * Lists up to 100 Firebase iOS apps associated with this Firebase project. * * @return The list of iOS apps. */ - listIosApps(): Promise; + listIosApps(): Promise; /** * Creates an `AndroidApp` object, referencing the specified Android app within @@ -299,7 +343,7 @@ export namespace admin.projectManagement { * * @return An `AndroidApp` object that references the specified Firebase Android app. */ - androidApp(appId: string): admin.projectManagement.AndroidApp; + androidApp(appId: string): AndroidApp; /** * Update the display name of this Firebase project. @@ -320,7 +364,7 @@ export namespace admin.projectManagement { * * @return An `iOSApp` object that references the specified Firebase iOS app. */ - iosApp(appId: string): admin.projectManagement.IosApp; + iosApp(appId: string): IosApp; /** * Creates a `ShaCertificate` object. @@ -331,7 +375,7 @@ export namespace admin.projectManagement { * * @return A `ShaCertificate` object contains the specified SHA hash. */ - shaCertificate(shaHash: string): admin.projectManagement.ShaCertificate; + shaCertificate(shaHash: string): ShaCertificate; /** * Creates a new Firebase Android app associated with this Firebase project. @@ -344,7 +388,7 @@ export namespace admin.projectManagement { * @return A promise that resolves to the newly created Android app. */ createAndroidApp( - packageName: string, displayName?: string): Promise; + packageName: string, displayName?: string): Promise; /** * Creates a new Firebase iOS app associated with this Firebase project. @@ -355,6 +399,6 @@ export namespace admin.projectManagement { * * @return A promise that resolves to the newly created iOS app. */ - createIosApp(bundleId: string, displayName?: string): Promise; + createIosApp(bundleId: string, displayName?: string): Promise; } } diff --git a/src/project-management/ios-app.ts b/src/project-management/ios-app.ts index 18acb47ae1..b29af6326f 100644 --- a/src/project-management/ios-app.ts +++ b/src/project-management/ios-app.ts @@ -16,10 +16,14 @@ import { FirebaseProjectManagementError } from '../utils/error'; import * as validator from '../utils/validator'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; -import { IosAppMetadata, AppPlatform } from './app-metadata'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; +import { projectManagement } from './index'; -export class IosApp { +import IosAppInterface = projectManagement.IosApp; +import IosAppMetadata = projectManagement.IosAppMetadata; +import AppPlatform = projectManagement.AppPlatform; + +export class IosApp implements IosAppInterface { private readonly resourceName: string; constructor( @@ -33,6 +37,12 @@ export class IosApp { this.resourceName = `projects/-/iosApps/${appId}`; } + /** + * Retrieves metadata about this iOS app. + * + * @return {!Promise} A promise that + * resolves to the retrieved metadata about this iOS app. + */ public getMetadata(): Promise { return this.requestHandler.getResource(this.resourceName) .then((responseData: any) => { @@ -61,13 +71,24 @@ export class IosApp { }); } + /** + * Sets the optional user-assigned display name of the app. + * + * @param newDisplayName The new display name to set. + * + * @return A promise that resolves when the display name has + * been set. + */ public setDisplayName(newDisplayName: string): Promise { return this.requestHandler.setDisplayName(this.resourceName, newDisplayName); } /** - * @return {Promise} A promise that resolves to a UTF-8 XML string, typically intended to - * be written to a plist file. + * Gets the configuration artifact associated with this app. + * + * @return A promise that resolves to the iOS app's Firebase + * config file, in UTF-8 string format. This string is typically intended to + * be written to a plist file that gets shipped with your iOS app. */ public getConfig(): Promise { return this.requestHandler.getConfig(this.resourceName) @@ -81,7 +102,7 @@ export class IosApp { assertServerResponse( validator.isBase64String(base64ConfigFileContents), responseData, - `getConfig()'s responseData.configFileContents must be a base64 string.`); + 'getConfig()\'s responseData.configFileContents must be a base64 string.'); return Buffer.from(base64ConfigFileContents, 'base64').toString('utf8'); }); diff --git a/src/project-management/project-management-api-request.ts b/src/project-management/project-management-api-request-internal.ts similarity index 96% rename from src/project-management/project-management-api-request.ts rename to src/project-management/project-management-api-request-internal.ts index c7528f9157..445faf83dc 100644 --- a/src/project-management/project-management-api-request.ts +++ b/src/project-management/project-management-api-request-internal.ts @@ -21,6 +21,7 @@ import { import { FirebaseProjectManagementError, ProjectManagementErrorCode } from '../utils/error'; import * as validator from '../utils/validator'; import { ShaCertificate } from './android-app'; +import { getSdkVersion } from '../utils/index'; /** Project management backend host and port. */ const PROJECT_MANAGEMENT_HOST_AND_PORT = 'firebase.googleapis.com:443'; @@ -30,7 +31,7 @@ const PROJECT_MANAGEMENT_PATH = '/v1/'; const PROJECT_MANAGEMENT_BETA_PATH = '/v1beta1/'; /** Project management request header. */ const PROJECT_MANAGEMENT_HEADERS = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, }; /** Project management request timeout duration in milliseconds. */ const PROJECT_MANAGEMENT_TIMEOUT_MILLIS = 10000; @@ -172,11 +173,11 @@ export class ProjectManagementRequestHandler { assertServerResponse( validator.isNonNullObject(responseData), responseData, - `createAndroidApp's responseData must be a non-null object.`); + 'createAndroidApp\'s responseData must be a non-null object.'); assertServerResponse( validator.isNonEmptyString(responseData.name), responseData, - `createAndroidApp's responseData.name must be a non-empty string.`); + 'createAndroidApp\'s responseData.name must be a non-empty string.'); return this.pollRemoteOperationWithExponentialBackoff(responseData.name); }); } @@ -199,11 +200,11 @@ export class ProjectManagementRequestHandler { assertServerResponse( validator.isNonNullObject(responseData), responseData, - `createIosApp's responseData must be a non-null object.`); + 'createIosApp\'s responseData must be a non-null object.'); assertServerResponse( validator.isNonEmptyString(responseData.name), responseData, - `createIosApp's responseData.name must be a non-empty string.`); + 'createIosApp\'s responseData.name must be a non-empty string.'); return this.pollRemoteOperationWithExponentialBackoff(responseData.name); }); } @@ -275,7 +276,7 @@ export class ProjectManagementRequestHandler { private pollRemoteOperationWithExponentialBackoff( operationResourceName: string): Promise { - const poller = new ExponentialBackoffPoller(); + const poller = new ExponentialBackoffPoller(); return poller.poll(() => { return this.invokeRequestHandler('GET', operationResourceName, /* requestData */ null) diff --git a/src/project-management/project-management.ts b/src/project-management/project-management.ts index dfe6be3a3f..9dc8b29902 100644 --- a/src/project-management/project-management.ts +++ b/src/project-management/project-management.ts @@ -15,36 +15,25 @@ */ import { FirebaseApp } from '../firebase-app'; -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseProjectManagementError } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { AndroidApp, ShaCertificate } from './android-app'; import { IosApp } from './ios-app'; -import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request'; -import { AppMetadata, AppPlatform } from './app-metadata'; +import { ProjectManagementRequestHandler, assertServerResponse } from './project-management-api-request-internal'; +import { projectManagement } from './index'; -/** - * Internals of a Project Management instance. - */ -class ProjectManagementInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise} An empty Promise that will be resolved when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} +import AppMetadata = projectManagement.AppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import ProjectManagementInterface = projectManagement.ProjectManagement; /** - * ProjectManagement service bound to the provided app. + * The Firebase ProjectManagement service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.projectManagement()`](projectManagement#projectManagement). */ -export class ProjectManagement implements FirebaseServiceInterface { - - public readonly INTERNAL: ProjectManagementInternals = new ProjectManagementInternals(); +export class ProjectManagement implements ProjectManagementInterface { private readonly requestHandler: ProjectManagementRequestHandler; private projectId: string; @@ -66,6 +55,8 @@ export class ProjectManagement implements FirebaseServiceInterface { /** * Lists up to 100 Firebase Android apps associated with this Firebase project. + * + * @return The list of Android apps. */ public listAndroidApps(): Promise { return this.listPlatformApps('android', 'listAndroidApps()'); @@ -73,34 +64,63 @@ export class ProjectManagement implements FirebaseServiceInterface { /** * Lists up to 100 Firebase iOS apps associated with this Firebase project. + * + * @return The list of iOS apps. */ public listIosApps(): Promise { return this.listPlatformApps('ios', 'listIosApps()'); } /** - * Returns an AndroidApp object for the given appId. No RPC is made. + * Creates an `AndroidApp` object, referencing the specified Android app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the Android app to reference. + * + * @return An `AndroidApp` object that references the specified Firebase Android app. */ public androidApp(appId: string): AndroidApp { return new AndroidApp(appId, this.requestHandler); } /** - * Returns an IosApp object for the given appId. No RPC is made. + * Creates an `iOSApp` object, referencing the specified iOS app within + * this Firebase project. + * + * This method does not perform an RPC. + * + * @param appId The `appId` of the iOS app to reference. + * + * @return An `iOSApp` object that references the specified Firebase iOS app. */ public iosApp(appId: string): IosApp { return new IosApp(appId, this.requestHandler); } /** - * Returns a ShaCertificate object for the given shaHash. No RPC is made. + * Creates a `ShaCertificate` object. + * + * This method does not perform an RPC. + * + * @param shaHash The SHA-1 or SHA-256 hash for this certificate. + * + * @return A `ShaCertificate` object contains the specified SHA hash. */ public shaCertificate(shaHash: string): ShaCertificate { return new ShaCertificate(shaHash); } /** - * Creates a new Firebase Android app, associated with this Firebase project. + * Creates a new Firebase Android app associated with this Firebase project. + * + * @param packageName The canonical package name of the Android App, + * as would appear in the Google Play Developer Console. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created Android app. */ public createAndroidApp(packageName: string, displayName?: string): Promise { return this.getResourceName() @@ -116,13 +136,19 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(responseData.appId), responseData, - `"responseData.appId" field must be present in createAndroidApp()'s response data.`); + '"responseData.appId" field must be present in createAndroidApp()\'s response data.'); return new AndroidApp(responseData.appId, this.requestHandler); }); } /** - * Creates a new Firebase iOS app, associated with this Firebase project. + * Creates a new Firebase iOS app associated with this Firebase project. + * + * @param bundleId The iOS app bundle ID to use for this new app. + * @param displayName An optional user-assigned display name for this + * new app. + * + * @return A promise that resolves to the newly created iOS app. */ public createIosApp(bundleId: string, displayName?: string): Promise { return this.getResourceName() @@ -138,13 +164,15 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(responseData.appId), responseData, - `"responseData.appId" field must be present in createIosApp()'s response data.`); + '"responseData.appId" field must be present in createIosApp()\'s response data.'); return new IosApp(responseData.appId, this.requestHandler); }); } /** * Lists up to 100 Firebase apps associated with this Firebase project. + * + * @return A promise that resolves to the metadata list of the apps. */ public listAppMetadata(): Promise { return this.getResourceName() @@ -160,7 +188,11 @@ export class ProjectManagement implements FirebaseServiceInterface { } /** - * Update display name of the project + * Update the display name of this Firebase project. + * + * @param newDisplayName The new display name to be updated. + * + * @return A promise that resolves when the project display name has been updated. */ public setDisplayName(newDisplayName: string): Promise { return this.getResourceName() @@ -180,11 +212,11 @@ export class ProjectManagement implements FirebaseServiceInterface { assertServerResponse( validator.isNonEmptyString(appJson.appId), responseData, - `"apps[].appId" field must be present in the listAppMetadata() response data.`); + '"apps[].appId" field must be present in the listAppMetadata() response data.'); assertServerResponse( validator.isNonEmptyString(appJson.platform), responseData, - `"apps[].platform" field must be present in the listAppMetadata() response data.`); + '"apps[].platform" field must be present in the listAppMetadata() response data.'); const metadata: AppMetadata = { appId: appJson.appId, platform: (AppPlatform as any)[appJson.platform] || AppPlatform.PLATFORM_UNKNOWN, diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts new file mode 100644 index 0000000000..668fee3372 --- /dev/null +++ b/src/remote-config/index.ts @@ -0,0 +1,399 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link remoteConfig.RemoteConfig `RemoteConfig`} service for the + * default app or a given app. + * + * `admin.remoteConfig()` can be called with no arguments to access the default + * app's {@link remoteConfig.RemoteConfig `RemoteConfig`} service or as + * `admin.remoteConfig(app)` to access the + * {@link remoteConfig.RemoteConfig `RemoteConfig`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for the default app + * var defaultRemoteConfig = admin.remoteConfig(); + * ``` + * + * @example + * ```javascript + * // Get the `RemoteConfig` service for a given app + * var otherRemoteConfig = admin.remoteConfig(otherApp); + * ``` + * + * @param app Optional app for which to return the `RemoteConfig` service. + * If not provided, the default `RemoteConfig` service is returned. + * + * @return The default `RemoteConfig` service if no + * app is provided, or the `RemoteConfig` service associated with the provided + * app. + */ +export declare function remoteConfig(app?: app.App): remoteConfig.RemoteConfig; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace remoteConfig { + /** + * Interface representing options for Remote Config list versions operation. + */ + export interface ListVersionsOptions { + /** + * The maximum number of items to return per page. + */ + pageSize?: number; + + /** + * The `nextPageToken` value returned from a previous list versions request, if any. + */ + pageToken?: string; + + /** + * Specifies the newest version number to include in the results. + * If specified, must be greater than zero. Defaults to the newest version. + */ + endVersionNumber?: string | number; + + /** + * Specifies the earliest update time to include in the results. Any entries updated before this + * time are omitted. + */ + startTime?: Date | string; + + /** + * Specifies the latest update time to include in the results. Any entries updated on or after + * this time are omitted. + */ + endTime?: Date | string; + } + + /** + * Interface representing a list of Remote Config template versions. + */ + export interface ListVersionsResult { + /** + * A list of version metadata objects, sorted in reverse chronological order. + */ + versions: Version[]; + + /** + * Token to retrieve the next page of results, or empty if there are no more results + * in the list. + */ + nextPageToken?: string; + } + + /** + * Interface representing a Remote Config condition. + * A condition targets a specific group of users. A list of these conditions make up + * part of a Remote Config template. + */ + export interface RemoteConfigCondition { + + /** + * A non-empty and unique name of this condition. + */ + name: string; + + /** + * The logic of this condition. + * See the documentation on + * {@link https://firebase.google.com/docs/remote-config/condition-reference condition expressions} + * for the expected syntax of this field. + */ + expression: string; + + /** + * The color associated with this condition for display purposes in the Firebase Console. + * Not specifying this value results in the console picking an arbitrary color to associate + * with the condition. + */ + tagColor?: TagColor; + } + + /** + * Interface representing a Remote Config parameter. + * At minimum, a `defaultValue` or a `conditionalValues` entry must be present for the + * parameter to have any effect. + */ + export interface RemoteConfigParameter { + + /** + * The value to set the parameter to, when none of the named conditions evaluate to `true`. + */ + defaultValue?: RemoteConfigParameterValue; + + /** + * A `(condition name, value)` map. The condition name of the highest priority + * (the one listed first in the Remote Config template's conditions list) determines the value of + * this parameter. + */ + conditionalValues?: { [key: string]: RemoteConfigParameterValue }; + + /** + * A description for this parameter. Should not be over 100 characters and may contain any + * Unicode characters. + */ + description?: string; + } + + /** + * Interface representing a Remote Config parameter group. + * Grouping parameters is only for management purposes and does not affect client-side + * fetching of parameter values. + */ + export interface RemoteConfigParameterGroup { + /** + * A description for the group. Its length must be less than or equal to 256 characters. + * A description may contain any Unicode characters. + */ + description?: string; + + /** + * Map of parameter keys to their optional default values and optional conditional values for + * parameters that belong to this group. A parameter only appears once per + * Remote Config template. An ungrouped parameter appears at the top level, whereas a + * parameter organized within a group appears within its group's map of parameters. + */ + parameters: { [key: string]: RemoteConfigParameter }; + } + + /** + * Interface representing an explicit parameter value. + */ + export interface ExplicitParameterValue { + /** + * The `string` value that the parameter is set to. + */ + value: string; + } + + /** + * Interface representing an in-app-default value. + */ + export interface InAppDefaultValue { + /** + * If `true`, the parameter is omitted from the parameter values returned to a client. + */ + useInAppDefault: boolean; + } + + /** + * Type representing a Remote Config parameter value. + * A `RemoteConfigParameterValue` could be either an `ExplicitParameterValue` or + * an `InAppDefaultValue`. + */ + export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; + + /** + * Interface representing a Remote Config template. + */ + export interface RemoteConfigTemplate { + /** + * A list of conditions in descending order by priority. + */ + conditions: RemoteConfigCondition[]; + + /** + * Map of parameter keys to their optional default values and optional conditional values. + */ + parameters: { [key: string]: RemoteConfigParameter }; + + /** + * Map of parameter group names to their parameter group objects. + * A group's name is mutable but must be unique among groups in the Remote Config template. + * The name is limited to 256 characters and intended to be human-readable. Any Unicode + * characters are allowed. + */ + parameterGroups: { [key: string]: RemoteConfigParameterGroup }; + + /** + * ETag of the current Remote Config template (readonly). + */ + readonly etag: string; + + /** + * Version information for the current Remote Config template. + */ + version?: Version; + } + + /** + * Interface representing a Remote Config user. + */ + export interface RemoteConfigUser { + /** + * Email address. Output only. + */ + email: string; + + /** + * Display name. Output only. + */ + name?: string; + + /** + * Image URL. Output only. + */ + imageUrl?: string; + } + + /** + * Colors that are associated with conditions for display purposes. + */ + export type TagColor = 'BLUE' | 'BROWN' | 'CYAN' | 'DEEP_ORANGE' | 'GREEN' | + 'INDIGO' | 'LIME' | 'ORANGE' | 'PINK' | 'PURPLE' | 'TEAL'; + + /** + * Interface representing a Remote Config template version. + * Output only, except for the version description. Contains metadata about a particular + * version of the Remote Config template. All fields are set at the time the specified Remote + * Config template is published. A version's description field may be specified in + * `publishTemplate` calls. + */ + export interface Version { + /** + * The version number of a Remote Config template. + */ + versionNumber?: string; + + /** + * The timestamp of when this version of the Remote Config template was written to the + * Remote Config backend. + */ + updateTime?: string; + + /** + * The origin of the template update action. + */ + updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | + 'REST_API' | 'ADMIN_SDK_NODE'); + + /** + * The type of the template update action. + */ + updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | + 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); + + /** + * Aggregation of all metadata fields about the account that performed the update. + */ + updateUser?: RemoteConfigUser; + + /** + * The user-provided description of the corresponding Remote Config template. + */ + description?: string; + + /** + * The version number of the Remote Config template that has become the current version + * due to a rollback. Only present if this version is the result of a rollback. + */ + rollbackSource?: string; + + /** + * Indicates whether this Remote Config template was published before version history was + * supported. + */ + isLegacy?: boolean; + } + + /** + * The Firebase `RemoteConfig` service interface. + */ + export interface RemoteConfig { + app: app.App; + + /** + * Gets the current active version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplate(): Promise; + + /** + * Gets the requested version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @param versionNumber Version number of the Remote Config template to look up. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ + getTemplateAtVersion(versionNumber: number | string): Promise; + + /** + * Validates a {@link remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. + * + * @param template The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. + */ + validateTemplate(template: RemoteConfigTemplate): Promise; + + /** + * Publishes a Remote Config template. + * + * @param template The Remote Config template to be published. + * @param options Optional options object when publishing a Remote Config template: + * - {boolean} `force` Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. This approach is not recommended + * because it risks causing the loss of updates to your Remote Config + * template if multiple clients are updating the Remote Config template. + * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + * ETag usage and forced updates}. + * + * @return A Promise that fulfills with the published `RemoteConfigTemplate`. + */ + publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise; + + /** + * Rolls back a project's published Remote Config template to the specified version. + * A rollback is equivalent to getting a previously published Remote Config + * template and re-publishing it using a force update. + * + * @param versionNumber The version number of the Remote Config template to roll back to. + * The specified version number must be lower than the current version number, and not have + * been deleted due to staleness. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * @return A promise that fulfills with the published `RemoteConfigTemplate`. + */ + rollback(versionNumber: string | number): Promise; + + /** + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * + * @param options Optional {@link remoteConfig.ListVersionsOptions `ListVersionsOptions`} + * object for getting a list of template versions. + * @return A promise that fulfills with a `ListVersionsResult`. + */ + listVersions(options?: ListVersionsOptions): Promise; + + /** + * Creates and returns a new Remote Config template from a JSON string. + * + * @param json The JSON string to populate a Remote Config template. + * + * @return A new template instance. + */ + createTemplateFromJSON(json: string): RemoteConfigTemplate; + } +} diff --git a/src/remote-config/remote-config-api-client.ts b/src/remote-config/remote-config-api-client-internal.ts similarity index 84% rename from src/remote-config/remote-config-api-client.ts rename to src/remote-config/remote-config-api-client-internal.ts index ed1d4d759f..ac2d071453 100644 --- a/src/remote-config/remote-config-api-client.ts +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -14,18 +14,22 @@ * limitations under the License. */ +import { remoteConfig } from './index'; import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient, HttpResponse } from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; -import { FirebaseRemoteConfigError, RemoteConfigErrorCode } from './remote-config-utils'; import { FirebaseApp } from '../firebase-app'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import ListVersionsOptions = remoteConfig.ListVersionsOptions; +import ListVersionsResult = remoteConfig.ListVersionsResult; + // Remote Config backend constants const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; const FIREBASE_REMOTE_CONFIG_HEADERS = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, // There is a known issue in which the ETag is not properly returned in cases where the request // does not specify a compression type. Currently, it is required to include the header // `Accept-Encoding: gzip` or equivalent in all requests. @@ -33,96 +37,6 @@ const FIREBASE_REMOTE_CONFIG_HEADERS = { 'Accept-Encoding': 'gzip', }; -export enum TagColor { - BLUE = "Blue", - BROWN = "Brown", - CYAN = "Cyan", - DEEP_ORANGE = "Red Orange", - GREEN = "Green", - INDIGO = "Indigo", - LIME = "Lime", - ORANGE = "Orange", - PINK = "Pink", - PURPLE = "Purple", - TEAL = "Teal", -} - -/** Interface representing a Remote Config parameter `value` in value options. */ -export interface ExplicitParameterValue { - value: string; -} - -/** Interface representing a Remote Config parameter `useInAppDefault` in value options. */ -export interface InAppDefaultValue { - useInAppDefault: boolean; -} - -export type RemoteConfigParameterValue = ExplicitParameterValue | InAppDefaultValue; - -/** Interface representing a Remote Config parameter. */ -export interface RemoteConfigParameter { - defaultValue?: RemoteConfigParameterValue; - conditionalValues?: { [key: string]: RemoteConfigParameterValue }; - description?: string; -} - -/** Interface representing a Remote Config parameter group. */ -export interface RemoteConfigParameterGroup { - description?: string; - parameters: { [key: string]: RemoteConfigParameter }; -} - -/** Interface representing a Remote Config condition. */ -export interface RemoteConfigCondition { - name: string; - expression: string; - tagColor?: TagColor; -} - -/** Interface representing a Remote Config template. */ -export interface RemoteConfigTemplate { - conditions: RemoteConfigCondition[]; - parameters: { [key: string]: RemoteConfigParameter }; - parameterGroups: { [key: string]: RemoteConfigParameterGroup }; - readonly etag: string; - version?: Version; -} - -/** Interface representing a Remote Config version. */ -export interface Version { - versionNumber?: string; // int64 format - updateTime?: string; // in UTC - updateOrigin?: ('REMOTE_CONFIG_UPDATE_ORIGIN_UNSPECIFIED' | 'CONSOLE' | - 'REST_API' | 'ADMIN_SDK_NODE'); - updateType?: ('REMOTE_CONFIG_UPDATE_TYPE_UNSPECIFIED' | - 'INCREMENTAL_UPDATE' | 'FORCED_UPDATE' | 'ROLLBACK'); - updateUser?: RemoteConfigUser; - description?: string; - rollbackSource?: string; - isLegacy?: boolean; -} - -/** Interface representing a list of Remote Config template versions. */ -export interface ListVersionsResult { - versions: Version[]; - nextPageToken?: string; -} - -/** Interface representing a Remote Config list version options. */ -export interface ListVersionsOptions { - pageSize?: number; - pageToken?: string; - endVersionNumber?: string | number; - startTime?: Date | string; - endTime?: Date | string; -} - -/** Interface representing a Remote Config user. */ -export interface RemoteConfigUser { - email: string; - name?: string; - imageUrl?: string; -} /** * Class that facilitates sending requests to the Firebase Remote Config backend API. @@ -130,7 +44,6 @@ export interface RemoteConfigUser { * @private */ export class RemoteConfigApiClient { - private readonly httpClient: HttpClient; private projectIdPrefix?: string; @@ -186,7 +99,7 @@ export class RemoteConfigApiClient { template = this.validateInputRemoteConfigTemplate(template); return this.sendPutRequest(template, template.etag, true) .then((resp) => { - // validating a template returns an etag with the suffix -0 means that your update + // validating a template returns an etag with the suffix -0 means that your update // was successfully validated. We set the etag back to the original etag of the template // to allow future operations. this.validateEtag(resp.headers['etag']); @@ -351,7 +264,7 @@ export class RemoteConfigApiClient { * Removes output only properties from version metadata. * * @param {RemoteConfigTemplate} template A RemoteConfigTemplate object to be validated. - * + * * @returns {RemoteConfigTemplate} The validated RemoteConfigTemplate object. */ private validateInputRemoteConfigTemplate(template: RemoteConfigTemplate): RemoteConfigTemplate { @@ -394,7 +307,7 @@ export class RemoteConfigApiClient { * If valid, returns the string representation of the provided version number. * * @param {string|number} versionNumber A version number to be validated. - * + * * @returns {string} The validated version number as a string. */ private validateVersionNumber(versionNumber: string | number, propertyName = 'versionNumber'): string { @@ -423,9 +336,9 @@ export class RemoteConfigApiClient { /** * Checks if a given `ListVersionsOptions` object is valid. If successful, creates a copy of the * options object and convert `startTime` and `endTime` to RFC3339 UTC "Zulu" format, if present. - * + * * @param {ListVersionsOptions} options An options object to be validated. - * + * * @return {ListVersionsOptions} A copy of the provided options object with timestamps converted * to UTC Zulu format. */ @@ -497,7 +410,7 @@ interface Error { const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { ABORTED: 'aborted', - ALREADY_EXISTS: `already-exists`, + ALREADY_EXISTS: 'already-exists', INVALID_ARGUMENT: 'invalid-argument', INTERNAL: 'internal-error', FAILED_PRECONDITION: 'failed-precondition', @@ -508,3 +421,29 @@ const ERROR_CODE_MAPPING: { [key: string]: RemoteConfigErrorCode } = { 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-utils.ts b/src/remote-config/remote-config-utils.ts deleted file mode 100644 index 523eeb10bd..0000000000 --- a/src/remote-config/remote-config-utils.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*! - * Copyright 2020 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { PrefixedFirebaseError } from '../utils/error'; - -export type RemoteConfigErrorCode = - 'aborted' - | 'already-exists' - | 'failed-precondition' - | 'internal-error' - | 'invalid-argument' - | 'not-found' - | 'out-of-range' - | 'permission-denied' - | 'resource-exhausted' - | 'unauthenticated' - | 'unknown-error'; - -/** - * Firebase Remote Config error code structure. This extends PrefixedFirebaseError. - * - * @param {RemoteConfigErrorCode} code The error code. - * @param {string} message The error message. - * @constructor - */ -export class FirebaseRemoteConfigError extends PrefixedFirebaseError { - constructor(code: RemoteConfigErrorCode, message: string) { - super('remote-config', code, message); - } -} diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index eface572db..d0b0046832 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -14,47 +14,30 @@ * limitations under the License. */ -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; -import { FirebaseRemoteConfigError } from './remote-config-utils'; -import { - RemoteConfigApiClient, - RemoteConfigTemplate, - RemoteConfigParameter, - RemoteConfigCondition, - RemoteConfigParameterGroup, - ListVersionsOptions, - ListVersionsResult, - RemoteConfigUser, - Version, -} from './remote-config-api-client'; - -/** - * Internals of an RemoteConfig service instance. - */ -class RemoteConfigInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up - return Promise.resolve(undefined); - } -} +import { remoteConfig } from './index'; +import { FirebaseRemoteConfigError, RemoteConfigApiClient } from './remote-config-api-client-internal'; + +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import RemoteConfigParameter = remoteConfig.RemoteConfigParameter; +import RemoteConfigCondition = remoteConfig.RemoteConfigCondition; +import RemoteConfigParameterGroup = remoteConfig.RemoteConfigParameterGroup; +import ListVersionsOptions = remoteConfig.ListVersionsOptions; +import ListVersionsResult = remoteConfig.ListVersionsResult; +import RemoteConfigUser = remoteConfig.RemoteConfigUser; +import Version = remoteConfig.Version; +import RemoteConfigInterface = remoteConfig.RemoteConfig; /** * Remote Config service bound to the provided app. */ -export class RemoteConfig implements FirebaseServiceInterface { - public readonly INTERNAL: RemoteConfigInternals = new RemoteConfigInternals(); +export class RemoteConfig implements RemoteConfigInterface { private readonly client: RemoteConfigApiClient; /** - * @param {FirebaseApp} app The app for this RemoteConfig service. + * @param app The app for this RemoteConfig service. * @constructor */ constructor(readonly app: FirebaseApp) { @@ -62,10 +45,11 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the current active version of the Remote Config template of the project. - * - * @return {Promise} A Promise that fulfills when the template is available. - */ + * Gets the current active version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ public getTemplate(): Promise { return this.client.getTemplate() .then((templateResponse) => { @@ -74,12 +58,13 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets the requested version of the Remote Config template of the project. - * - * @param {number | string} versionNumber Version number of the Remote Config template to look up. - * - * @return {Promise} A Promise that fulfills when the template is available. - */ + * Gets the requested version of the {@link remoteConfig.RemoteConfigTemplate + * `RemoteConfigTemplate`} of the project. + * + * @param versionNumber Version number of the Remote Config template to look up. + * + * @return A promise that fulfills with a `RemoteConfigTemplate`. + */ public getTemplateAtVersion(versionNumber: number | string): Promise { return this.client.getTemplateAtVersion(versionNumber) .then((templateResponse) => { @@ -88,11 +73,10 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Validates a Remote Config template. + * Validates a {@link remoteConfig.RemoteConfigTemplate `RemoteConfigTemplate`}. * - * @param {RemoteConfigTemplate} template The Remote Config template to be validated. - * - * @return {Promise} A Promise that fulfills when a template is validated. + * @param template The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. */ public validateTemplate(template: RemoteConfigTemplate): Promise { return this.client.validateTemplate(template) @@ -104,10 +88,16 @@ export class RemoteConfig implements FirebaseServiceInterface { /** * Publishes a Remote Config template. * - * @param {RemoteConfigTemplate} template The Remote Config template to be validated. - * @param {any=} options Optional options object when publishing a Remote Config template. + * @param template The Remote Config template to be published. + * @param options Optional options object when publishing a Remote Config template: + * - {boolean} `force` Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. This approach is not recommended + * because it risks causing the loss of updates to your Remote Config + * template if multiple clients are updating the Remote Config template. + * See {@link https://firebase.google.com/docs/remote-config/use-config-rest#etag_usage_and_forced_updates + * ETag usage and forced updates}. * - * @return {Promise} A Promise that fulfills when a template is published. + * @return A Promise that fulfills with the published `RemoteConfigTemplate`. */ public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean }): Promise { return this.client.publishTemplate(template, options) @@ -117,13 +107,16 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Rollbacks a project's published Remote Config template to the specified version. + * Rolls back a project's published Remote Config template to the specified version. * A rollback is equivalent to getting a previously published Remote Config - * template, and re-publishing it using a force update. + * template and re-publishing it using a force update. * - * @param {number | string} versionNumber The version number of the Remote Config template - * to rollback to. - * @return {Promise} A Promise that fulfills with the published template. + * @param versionNumber The version number of the Remote Config template to roll back to. + * The specified version number must be lower than the current version number, and not have + * been deleted due to staleness. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (that is, all except the + * template that is being fetched by clients) are also deleted if they are more than 90 days old. + * @return A promise that fulfills with the published `RemoteConfigTemplate`. */ public rollback(versionNumber: number | string): Promise { return this.client.rollback(versionNumber) @@ -133,14 +126,14 @@ export class RemoteConfig implements FirebaseServiceInterface { } /** - * Gets a list of Remote Config template versions that have been published, sorted in reverse - * chronological order. Only the last 300 versions are stored. - * All versions that correspond to non-active Remote Config templates (i.e., all except the - * template that is being fetched by clients) are also deleted if they are older than 90 days. - * - * @param {ListVersionsOptions} options Optional options object for getting a list of versions. - * @return A promise that fulfills with a `ListVersionsResult`. - */ + * Gets a list of Remote Config template versions that have been published, sorted in reverse + * chronological order. Only the last 300 versions are stored. + * All versions that correspond to non-active Remote Config templates (i.e., all except the + * template that is being fetched by clients) are also deleted if they are older than 90 days. + * + * @param options Optional options object for getting a list of versions. + * @return A promise that fulfills with a `ListVersionsResult`. + */ public listVersions(options?: ListVersionsOptions): Promise { return this.client.listVersions(options) .then((listVersionsResponse) => { @@ -154,9 +147,9 @@ export class RemoteConfig implements FirebaseServiceInterface { /** * Creates and returns a new Remote Config template from a JSON string. * - * @param {string} json The JSON string to populate a Remote Config template. + * @param json The JSON string to populate a Remote Config template. * - * @return {RemoteConfigTemplate} A new template instance. + * @return A new template instance. */ public createTemplateFromJSON(json: string): RemoteConfigTemplate { if (!validator.isNonEmptyString(json)) { @@ -352,20 +345,16 @@ class VersionImpl implements Version { this.isLegacy = version.isLegacy; } - // The backend API provides timestamps as ISO date strings. The Admin SDK exposes timestamps - // as UTC date strings. If a developer uses a previously obtained template with UTC timestamps + // The backend API provides timestamps in ISO date strings. The Admin SDK exposes timestamps + // in UTC date strings. If a developer uses a previously obtained template with UTC timestamps // we could still validate it below. if (typeof version.updateTime !== 'undefined') { - if (!validator.isISODateString(version.updateTime) && - !validator.isUTCDateString(version.updateTime)) { + if (!this.isValidTimestamp(version.updateTime)) { throw new FirebaseRemoteConfigError( 'invalid-argument', 'Version update time must be a valid date string'); } - if (validator.isISODateString(version.updateTime)) { - // timestamps in output `Version` obtained from the API should be in UTC. - this.updateTime = new Date(version.updateTime).toUTCString(); - } + this.updateTime = new Date(version.updateTime).toUTCString(); } } @@ -384,4 +373,10 @@ class VersionImpl implements Version { 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.d.ts b/src/security-rules/index.ts similarity index 56% rename from src/security-rules.d.ts rename to src/security-rules/index.ts index f07a737431..2871f7873b 100644 --- a/src/security-rules.d.ts +++ b/src/security-rules/index.ts @@ -1,13 +1,60 @@ -import * as _admin from './index.d'; - -export namespace admin.securityRules { +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link securityRules.SecurityRules + * `SecurityRules`} service for the default app or a given app. + * + * `admin.securityRules()` can be called with no arguments to access the + * default app's {@link securityRules.SecurityRules + * `SecurityRules`} service, or as `admin.securityRules(app)` to access + * the {@link securityRules.SecurityRules `SecurityRules`} + * service associated with a specific app. + * + * @example + * ```javascript + * // Get the SecurityRules service for the default app + * var defaultSecurityRules = admin.securityRules(); + * ``` + * + * @example + * ```javascript + * // Get the SecurityRules service for a given app + * var otherSecurityRules = admin.securityRules(otherApp); + * ``` + * + * @param app Optional app to return the `SecurityRules` service + * for. If not provided, the default `SecurityRules` service + * is returned. + * @return The default `SecurityRules` service if no app is provided, or the + * `SecurityRules` service associated with the provided app. + */ +export declare function securityRules(app?: app.App): securityRules.SecurityRules; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace securityRules { /** * A source file containing some Firebase security rules. The content includes raw * source code including text formatting, indentation and comments. Use the - * [`securityRules.createRulesFileFromSource()`](admin.securityRules.SecurityRules#createRulesFileFromSource) + * [`securityRules.createRulesFileFromSource()`](securityRules.SecurityRules#createRulesFileFromSource) * method to create new instances of this type. */ - interface RulesFile { + export interface RulesFile { readonly name: string; readonly content: string; } @@ -15,14 +62,13 @@ export namespace admin.securityRules { /** * Required metadata associated with a ruleset. */ - interface RulesetMetadata { + export interface RulesetMetadata { /** * Name of the `Ruleset` as a short string. This can be directly passed into APIs - * like [`securityRules.getRuleset()`](admin.securityRules.SecurityRules#getRuleset) - * and [`securityRules.deleteRuleset()`](admin.securityRules.SecurityRules#deleteRuleset). + * like {@link securityRules.SecurityRules.getRuleset `securityRules.getRuleset()`} + * and {@link securityRules.SecurityRules.deleteRuleset `securityRules.deleteRuleset()`}. */ readonly name: string; - /** * Creation time of the `Ruleset` as a UTC timestamp string. */ @@ -30,35 +76,34 @@ export namespace admin.securityRules { } /** - * A set of Firebase security rules. + * A page of ruleset metadata. */ - interface Ruleset extends RulesetMetadata { - readonly source: admin.securityRules.RulesFile[]; - } - - interface RulesetMetadataList { + export interface RulesetMetadataList { /** * A batch of ruleset metadata. */ - readonly rulesets: admin.securityRules.RulesetMetadata[]; - + readonly rulesets: RulesetMetadata[]; /** * The next page token if available. This is needed to retrieve the next batch. */ readonly nextPageToken?: string; } + /** + * A set of Firebase security rules. + */ + export interface Ruleset extends RulesetMetadata { + readonly source: RulesFile[]; + } + /** * The Firebase `SecurityRules` service interface. - * - * Do not call this constructor directly. Instead, use - * [`admin.securityRules()`](admin.securityRules#securityRules). */ - interface SecurityRules { - app: _admin.app.App; + export interface SecurityRules { + app: app.App; /** - * Creates a {@link admin.securityRules.RulesFile `RuleFile`} with the given name + * Creates a {@link securityRules.RulesFile `RuleFile`} with the given name * and source. Throws an error if any of the arguments are invalid. This is a local * operation, and does not involve any network API calls. * @@ -74,19 +119,19 @@ export namespace admin.securityRules { * @param source Contents of the rules file. * @return A new rules file instance. */ - createRulesFileFromSource(name: string, source: string | Buffer): admin.securityRules.RulesFile; + createRulesFileFromSource(name: string, source: string | Buffer): RulesFile; /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given - * {@link admin.securityRules.RulesFile `RuleFile`}. + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * {@link securityRules.RulesFile `RuleFile`}. * * @param file Rules file to include in the new `Ruleset`. * @returns A promise that fulfills with the newly created `Ruleset`. */ - createRuleset(file: admin.securityRules.RulesFile): Promise; + createRuleset(file: RulesFile): Promise; /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given + * Gets the {@link securityRules.Ruleset `Ruleset`} identified by the given * name. The input name should be the short name string without the project ID * prefix. For example, to retrieve the `projects/project-id/rulesets/my-ruleset`, * pass the short name "my-ruleset". Rejects with a `not-found` error if the @@ -95,10 +140,10 @@ export namespace admin.securityRules { * @param name Name of the `Ruleset` to retrieve. * @return A promise that fulfills with the specified `Ruleset`. */ - getRuleset(name: string): Promise; + getRuleset(name: string): Promise; /** - * Deletes the {@link admin.securityRules.Ruleset `Ruleset`} identified by the given + * Deletes the {@link securityRules.Ruleset `Ruleset`} identified by the given * name. The input name should be the short name string without the project ID * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, * pass the short name "my-ruleset". Rejects with a `not-found` error if the @@ -119,38 +164,38 @@ export namespace admin.securityRules { * @return A promise that fulfills with a page of rulesets. */ listRulesetMetadata( - pageSize?: number, nextPageToken?: string): Promise; + pageSize?: number, nextPageToken?: string): Promise; /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} currently applied to + * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to * Cloud Firestore. Rejects with a `not-found` error if no ruleset is applied * on Firestore. * * @return A promise that fulfills with the Firestore ruleset. */ - getFirestoreRuleset(): Promise; + getFirestoreRuleset(): Promise; /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given * source, and applies it to Cloud Firestore. * * @param source Rules source to apply. * @return A promise that fulfills when the ruleset is created and released. */ - releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; + releaseFirestoreRulesetFromSource(source: string | Buffer): Promise; /** - * Applies the specified {@link admin.securityRules.Ruleset `Ruleset`} ruleset + * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset * to Cloud Firestore. * * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object * containing the name. * @return A promise that fulfills when the ruleset is released. */ - releaseFirestoreRuleset(ruleset: string | admin.securityRules.RulesetMetadata): Promise; + releaseFirestoreRuleset(ruleset: string | RulesetMetadata): Promise; /** - * Gets the {@link admin.securityRules.Ruleset `Ruleset`} currently applied to a + * Gets the {@link securityRules.Ruleset `Ruleset`} currently applied to a * Cloud Storage bucket. Rejects with a `not-found` error if no ruleset is applied * on the bucket. * @@ -159,33 +204,33 @@ export namespace admin.securityRules { * `AppOptions`. * @return A promise that fulfills with the Cloud Storage ruleset. */ - getStorageRuleset(bucket?: string): Promise; + getStorageRuleset(bucket?: string): Promise; /** - * Creates a new {@link admin.securityRules.Ruleset `Ruleset`} from the given + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given * source, and applies it to a Cloud Storage bucket. * * @param source Rules source to apply. * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If * not specified, applies the ruleset on the default bucket configured via - * {@link admin.AppOptions `AppOptions`}. + * {@link AppOptions `AppOptions`}. * @return A promise that fulfills when the ruleset is created and released. */ releaseStorageRulesetFromSource( - source: string | Buffer, bucket?: string): Promise; + source: string | Buffer, bucket?: string): Promise; /** - * Applies the specified {@link admin.securityRules.Ruleset `Ruleset`} ruleset + * Applies the specified {@link securityRules.Ruleset `Ruleset`} ruleset * to a Cloud Storage bucket. * * @param ruleset Name of the ruleset to apply or a `RulesetMetadata` object * containing the name. * @param bucket Optional name of the Cloud Storage bucket to apply the rules on. If * not specified, applies the ruleset on the default bucket configured via - * {@link admin.AppOptions `AppOptions`}. + * {@link AppOptions `AppOptions`}. * @return A promise that fulfills when the ruleset is released. */ releaseStorageRuleset( - ruleset: string | admin.securityRules.RulesetMetadata, bucket?: string): Promise; + ruleset: string | RulesetMetadata, bucket?: string): Promise; } } diff --git a/src/security-rules/security-rules-api-client.ts b/src/security-rules/security-rules-api-client-internal.ts similarity index 98% rename from src/security-rules/security-rules-api-client.ts rename to src/security-rules/security-rules-api-client-internal.ts index 37989081fb..50fdbc9cb8 100644 --- a/src/security-rules/security-rules-api-client.ts +++ b/src/security-rules/security-rules-api-client-internal.ts @@ -16,14 +16,14 @@ import { HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient } from '../utils/api-request'; import { PrefixedFirebaseError } from '../utils/error'; -import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-utils'; +import { FirebaseSecurityRulesError, SecurityRulesErrorCode } from './security-rules-internal'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { FirebaseApp } from '../firebase-app'; const RULES_V1_API = 'https://firebaserules.googleapis.com/v1'; const FIREBASE_VERSION_HEADER = { - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, }; export interface Release { @@ -170,7 +170,7 @@ export class SecurityRulesApiClient { const request: HttpRequestConfig = { method: 'PATCH', url: `${url}/releases/${name}`, - data: {release}, + data: { release }, }; return this.sendRequest(request); }); diff --git a/src/security-rules/security-rules-utils.ts b/src/security-rules/security-rules-internal.ts similarity index 100% rename from src/security-rules/security-rules-utils.ts rename to src/security-rules/security-rules-internal.ts diff --git a/src/security-rules/security-rules.ts b/src/security-rules/security-rules.ts index 21da0eabab..2206410df0 100644 --- a/src/security-rules/security-rules.ts +++ b/src/security-rules/security-rules.ts @@ -14,37 +14,19 @@ * limitations under the License. */ -import { FirebaseServiceInterface, FirebaseServiceInternalsInterface } from '../firebase-service'; import { FirebaseApp } from '../firebase-app'; import * as validator from '../utils/validator'; import { SecurityRulesApiClient, RulesetResponse, RulesetContent, ListRulesetsResponse, -} from './security-rules-api-client'; -import { FirebaseSecurityRulesError } from './security-rules-utils'; +} from './security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from './security-rules-internal'; +import { securityRules } from './index'; -/** - * A source file containing some Firebase security rules. - */ -export interface RulesFile { - readonly name: string; - readonly content: string; -} - -/** - * Additional metadata associated with a Ruleset. - */ -export interface RulesetMetadata { - readonly name: string; - readonly createTime: string; -} - -/** - * A page of ruleset metadata. - */ -export interface RulesetMetadataList { - readonly rulesets: RulesetMetadata[]; - readonly nextPageToken?: string; -} +import RulesFile = securityRules.RulesFile; +import RulesetMetadata = securityRules.RulesetMetadata; +import RulesetMetadataList = securityRules.RulesetMetadataList; +import RulesetInterface = securityRules.Ruleset; +import SecurityRulesInterface = securityRules.SecurityRules; class RulesetMetadataListImpl implements RulesetMetadataList { @@ -74,7 +56,7 @@ class RulesetMetadataListImpl implements RulesetMetadataList { /** * Represents a set of Firebase security rules. */ -export class Ruleset implements RulesetMetadata { +export class Ruleset implements RulesetInterface { public readonly name: string; public readonly createTime: string; @@ -97,15 +79,16 @@ export class Ruleset implements RulesetMetadata { } /** - * SecurityRules service bound to the provided app. + * The Firebase `SecurityRules` service interface. + * + * Do not call this constructor directly. Instead, use + * [`admin.securityRules()`](securityRules#securityRules). */ -export class SecurityRules implements FirebaseServiceInterface { +export class SecurityRules implements SecurityRulesInterface { private static readonly CLOUD_FIRESTORE = 'cloud.firestore'; private static readonly FIREBASE_STORAGE = 'firebase.storage'; - public readonly INTERNAL = new SecurityRulesInternals(); - private readonly client: SecurityRulesApiClient; /** @@ -235,12 +218,21 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a `RulesFile` with the given name and source. Throws if any of the arguments are invalid. This is a - * local operation, and does not involve any network API calls. + * Creates a {@link securityRules.RulesFile `RuleFile`} with the given name + * and source. Throws an error if any of the arguments are invalid. This is a local + * operation, and does not involve any network API calls. + * + * @example + * ```javascript + * const source = '// Some rules source'; + * const rulesFile = admin.securityRules().createRulesFileFromSource( + * 'firestore.rules', source); + * ``` * - * @param {string} name Name to assign to the rules file. - * @param {string|Buffer} source Contents of the rules file. - * @returns {RulesFile} A new rules file instance. + * @param name Name to assign to the rules file. This is usually a short file name that + * helps identify the file in a ruleset. + * @param source Contents of the rules file. + * @return A new rules file instance. */ public createRulesFileFromSource(name: string, source: string | Buffer): RulesFile { if (!validator.isNonEmptyString(name)) { @@ -265,10 +257,11 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Creates a new `Ruleset` from the given `RulesFile`. + * Creates a new {@link securityRules.Ruleset `Ruleset`} from the given + * {@link securityRules.RulesFile `RuleFile`}. * - * @param {RulesFile} file Rules file to include in the new Ruleset. - * @returns {Promise} A promise that fulfills with the newly created Ruleset. + * @param file Rules file to include in the new `Ruleset`. + * @returns A promise that fulfills with the newly created `Ruleset`. */ public createRuleset(file: RulesFile): Promise { const ruleset: RulesetContent = { @@ -284,24 +277,27 @@ export class SecurityRules implements FirebaseServiceInterface { } /** - * Deletes the Ruleset identified by the given name. The input name should be the short name string without - * the project ID prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, pass the - * short name "my-ruleset". Rejects with a `not-found` error if the specified Ruleset cannot be found. + * Deletes the {@link securityRules.Ruleset `Ruleset`} identified by the given + * name. The input name should be the short name string without the project ID + * prefix. For example, to delete the `projects/project-id/rulesets/my-ruleset`, + * pass the short name "my-ruleset". Rejects with a `not-found` error if the + * specified `Ruleset` cannot be found. * - * @param {string} name Name of the Ruleset to delete. - * @returns {Promise} A promise that fulfills when the Ruleset is deleted. + * @param name Name of the `Ruleset` to delete. + * @return A promise that fulfills when the `Ruleset` is deleted. */ public deleteRuleset(name: string): Promise { return this.client.deleteRuleset(name); } /** - * Retrieves a page of rulesets. + * Retrieves a page of ruleset metadata. * - * @param {number=} pageSize The page size, 100 if undefined. This is also the maximum allowed limit. - * @param {string=} nextPageToken The next page token. If not specified, returns rulesets starting - * without any offset. - * @returns {Promise} A promise that fulfills a page of rulesets. + * @param pageSize The page size, 100 if undefined. This is also the maximum allowed + * limit. + * @param nextPageToken The next page token. If not specified, returns rulesets + * starting without any offset. + * @return A promise that fulfills with a page of rulesets. */ public listRulesetMetadata(pageSize = 100, nextPageToken?: string): Promise { return this.client.listRulesets(pageSize, nextPageToken) @@ -353,12 +349,6 @@ export class SecurityRules implements FirebaseServiceInterface { } } -class SecurityRulesInternals implements FirebaseServiceInternalsInterface { - public delete(): Promise { - return Promise.resolve(); - } -} - 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..109f54431d --- /dev/null +++ b/src/storage/index.ts @@ -0,0 +1,63 @@ +/*! + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Bucket } from '@google-cloud/storage'; +import { app } from '../firebase-namespace-api'; + +/** + * Gets the {@link storage.Storage `Storage`} service for the + * default app or a given app. + * + * `admin.storage()` can be called with no arguments to access the default + * app's {@link storage.Storage `Storage`} service or as + * `admin.storage(app)` to access the + * {@link storage.Storage `Storage`} service associated with a + * specific app. + * + * @example + * ```javascript + * // Get the Storage service for the default app + * var defaultStorage = admin.storage(); + * ``` + * + * @example + * ```javascript + * // Get the Storage service for a given app + * var otherStorage = admin.storage(otherApp); + * ``` + */ +export declare function storage(app?: app.App): storage.Storage; + +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace storage { + /** + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. + */ + export interface Storage { + /** + * Optional app whose `Storage` service to + * return. If not provided, the default `Storage` service will be returned. + */ + app: app.App; + /** + * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) + * instance as defined in the `@google-cloud/storage` package. + */ + bucket(name?: string): Bucket; + } +} diff --git a/src/storage/storage.ts b/src/storage/storage.ts index afd3b003de..7bac81ff46 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,35 +15,22 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {FirebaseError} from '../utils/error'; -import {FirebaseServiceInterface, FirebaseServiceInternalsInterface} from '../firebase-service'; -import {ServiceAccountCredential, isApplicationDefault} from '../auth/credential'; -import {Bucket, Storage as StorageClient} from '@google-cloud/storage'; - +import { FirebaseApp } from '../firebase-app'; +import { FirebaseError } from '../utils/error'; +import { ServiceAccountCredential, isApplicationDefault } from '../credential/credential-internal'; +import { Bucket, Storage as StorageClient } from '@google-cloud/storage'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; +import { storage } from './index'; -/** - * Internals of a Storage instance. - */ -class StorageInternals implements FirebaseServiceInternalsInterface { - /** - * Deletes the service and its associated resources. - * - * @return {Promise<()>} An empty Promise that will be fulfilled when the service is deleted. - */ - public delete(): Promise { - // There are no resources to clean up. - return Promise.resolve(); - } -} +import StorageInterface = storage.Storage; /** - * Storage service bound to the provided app. + * The default `Storage` service if no + * app is provided or the `Storage` service associated with the provided + * app. */ -export class Storage implements FirebaseServiceInterface { - public readonly INTERNAL: StorageInternals = new StorageInternals(); +export class Storage implements StorageInterface { private readonly appInternal: FirebaseApp; private readonly storageClient: StorageClient; @@ -50,6 +38,7 @@ export class Storage implements FirebaseServiceInterface { /** * @param {FirebaseApp} app The app for this Storage service. * @constructor + * @internal */ constructor(app: FirebaseApp) { if (!validator.isNonNullObject(app) || !('options' in app)) { @@ -59,6 +48,19 @@ export class Storage implements FirebaseServiceInterface { }); } + 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 { storage = require('@google-cloud/storage').Storage; @@ -98,12 +100,10 @@ export class Storage implements FirebaseServiceInterface { } /** - * Returns a reference to a Google Cloud Storage bucket. Returned reference can be used to upload - * and download content from Google Cloud Storage. - * - * @param {string=} name Optional name of the bucket to be retrieved. If name is not specified, - * retrieves a reference to the default bucket. - * @return {Bucket} A Bucket object from the @google-cloud/storage library. + * @param name Optional name of the bucket to be retrieved. If name is not specified, + * retrieves a reference to the default bucket. + * @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket) + * instance as defined in the `@google-cloud/storage` package. */ public bucket(name?: string): Bucket { const bucketName = (typeof name !== 'undefined') @@ -120,9 +120,7 @@ export class Storage implements FirebaseServiceInterface { } /** - * Returns the app associated with this Storage instance. - * - * @return {FirebaseApp} The app associated with this Storage instance. + * @return The app associated with this Storage instance. */ get app(): FirebaseApp { return this.appInternal; diff --git a/src/utils/api-request.ts b/src/utils/api-request.ts index 9f1b8c18ca..5b1b153288 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,15 +15,15 @@ * limitations under the License. */ -import {FirebaseApp} from '../firebase-app'; -import {AppErrorCodes, FirebaseAppError} from './error'; +import { FirebaseApp } from '../firebase-app'; +import { AppErrorCodes, FirebaseAppError } from './error'; import * as validator from './validator'; import http = require('http'); import https = require('https'); import url = require('url'); -import {EventEmitter} from 'events'; -import {Readable} from 'stream'; +import { EventEmitter } from 'events'; +import { Readable } from 'stream'; import * as zlibmod from 'zlib'; /** Http method type definition. */ @@ -496,11 +497,15 @@ class AsyncHttpCall { }); const timeout: number | undefined = this.config.timeout; + const timeoutCallback: () => void = () => { + req.abort(); + this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req); + }; if (timeout) { // Listen to timeouts and throw an error. - req.setTimeout(timeout, () => { - req.abort(); - this.rejectWithError(`timeout of ${timeout}ms exceeded`, 'ETIMEDOUT', req); + req.setTimeout(timeout, timeoutCallback); + req.on('socket', (socket) => { + socket.setTimeout(timeout, timeoutCallback); }); } @@ -584,7 +589,7 @@ class AsyncHttpCall { response: LowLevelResponse, respStream: Readable, boundary: string): void { const dicer = require('dicer'); // eslint-disable-line @typescript-eslint/no-var-requires - const multipartParser = new dicer({boundary}); + const multipartParser = new dicer({ boundary }); const responseBuffer: Buffer[] = []; multipartParser.on('part', (part: any) => { const tempBuffers: Buffer[] = []; @@ -723,7 +728,7 @@ class HttpRequestConfigImpl implements HttpRequestConfig { public buildRequestOptions(): https.RequestOptions { const parsed = this.buildUrl(); const protocol = parsed.protocol; - let port: string | undefined = parsed.port; + let port: string | null = parsed.port; if (!port) { const isHttps = protocol === 'https:'; port = isHttps ? '443' : '80'; @@ -810,11 +815,11 @@ export class AuthorizedHttpClient extends HttpClient { } public send(request: HttpRequestConfig): Promise { - return this.app.INTERNAL.getToken().then((accessTokenObj) => { + return this.getToken().then((token) => { const requestCopy = Object.assign({}, request); requestCopy.headers = Object.assign({}, request.headers); const authHeader = 'Authorization'; - requestCopy.headers[authHeader] = `Bearer ${accessTokenObj.accessToken}`; + requestCopy.headers[authHeader] = `Bearer ${token}`; if (!requestCopy.httpAgent && this.app.options.httpAgent) { requestCopy.httpAgent = this.app.options.httpAgent; @@ -822,6 +827,13 @@ export class AuthorizedHttpClient extends HttpClient { return super.send(requestCopy); }); } + + protected getToken(): Promise { + return this.app.INTERNAL.getToken() + .then((accessTokenObj) => { + return accessTokenObj.accessToken; + }); + } } /** @@ -905,15 +917,15 @@ export class ApiSettings { * }); * ``` */ -export class ExponentialBackoffPoller extends EventEmitter { +export class ExponentialBackoffPoller extends EventEmitter { private numTries = 0; private completed = false; private masterTimer: NodeJS.Timer; private repollTimer: NodeJS.Timer; - private pollCallback?: () => Promise; - private resolve: (result: object) => void; + private pollCallback?: () => Promise; + private resolve: (result: T) => void; private reject: (err: object) => void; constructor( @@ -926,13 +938,13 @@ export class ExponentialBackoffPoller extends EventEmitter { /** * Poll the provided callback with exponential backoff. * - * @param {() => Promise} callback The callback to be called for each poll. If the + * @param {() => Promise} callback The callback to be called for each poll. If the * callback resolves to a falsey value, polling will continue. Otherwise, the truthy * resolution will be used to resolve the promise returned by this method. - * @return {Promise} A Promise which resolves to the truthy value returned by the provided + * @return {Promise} A Promise which resolves to the truthy value returned by the provided * callback when polling is complete. */ - public poll(callback: () => Promise): Promise { + public poll(callback: () => Promise): Promise { if (this.pollCallback) { throw new Error('poll() can only be called once per instance of ExponentialBackoffPoller'); } @@ -949,7 +961,7 @@ export class ExponentialBackoffPoller extends EventEmitter { this.reject(new Error('ExponentialBackoffPoller deadline exceeded - Master timeout reached')); }, this.masterTimeoutMillis); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; this.repoll(); diff --git a/src/utils/crypto-signer.ts b/src/utils/crypto-signer.ts new file mode 100644 index 0000000000..e8bb8b79ae --- /dev/null +++ b/src/utils/crypto-signer.ts @@ -0,0 +1,250 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FirebaseApp } from '../firebase-app'; +import { ServiceAccountCredential } from '../credential/credential-internal'; +import { AuthorizedHttpClient, HttpRequestConfig, HttpClient, HttpError } from './api-request'; + +import { Algorithm } from 'jsonwebtoken'; +import { ErrorInfo } from '../utils/error'; +import * as validator from '../utils/validator'; + +const ALGORITHM_RS256: Algorithm = 'RS256' as const; + +/** + * CryptoSigner interface represents an object that can be used to sign JWTs. + */ +export interface CryptoSigner { + + /** + * The name of the signing algorithm. + */ + readonly algorithm: Algorithm; + + /** + * Cryptographically signs a buffer of data. + * + * @param {Buffer} buffer The data to be signed. + * @return {Promise} A promise that resolves with the raw bytes of a signature. + */ + sign(buffer: Buffer): Promise; + + /** + * Returns the ID of the service account used to sign tokens. + * + * @return {Promise} A promise that resolves with a service account ID. + */ + getAccountId(): Promise; +} + +/** + * A CryptoSigner implementation that uses an explicitly specified service account private key to + * sign data. Performs all operations locally, and does not make any RPC calls. + */ +export class ServiceAccountSigner implements CryptoSigner { + + algorithm = ALGORITHM_RS256; + + /** + * Creates a new CryptoSigner instance from the given service account credential. + * + * @param {ServiceAccountCredential} credential A service account credential. + */ + constructor(private readonly credential: ServiceAccountCredential) { + if (!credential) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_CREDENTIAL, + message: 'INTERNAL ASSERT: Must provide a service account credential to initialize ServiceAccountSigner.', + }); + } + } + + /** + * @inheritDoc + */ + public sign(buffer: Buffer): Promise { + const crypto = require('crypto'); // eslint-disable-line @typescript-eslint/no-var-requires + const sign = crypto.createSign('RSA-SHA256'); + sign.update(buffer); + return Promise.resolve(sign.sign(this.credential.privateKey)); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + return Promise.resolve(this.credential.clientEmail); + } +} + +/** + * A CryptoSigner implementation that uses the remote IAM service to sign data. If initialized without + * a service account ID, attempts to discover a service account ID by consulting the local Metadata + * service. This will succeed in managed environments like Google Cloud Functions and App Engine. + * + * @see https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signBlob + * @see https://cloud.google.com/compute/docs/storing-retrieving-metadata + */ +export class IAMSigner implements CryptoSigner { + algorithm = ALGORITHM_RS256; + + private readonly httpClient: AuthorizedHttpClient; + private serviceAccountId?: string; + + constructor(httpClient: AuthorizedHttpClient, serviceAccountId?: string) { + if (!httpClient) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'INTERNAL ASSERT: Must provide a HTTP client to initialize IAMSigner.', + }); + } + if (typeof serviceAccountId !== 'undefined' && !validator.isNonEmptyString(serviceAccountId)) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'INTERNAL ASSERT: Service account ID must be undefined or a non-empty string.', + }); + } + this.httpClient = httpClient; + this.serviceAccountId = serviceAccountId; + } + + /** + * @inheritDoc + */ + public sign(buffer: Buffer): Promise { + return this.getAccountId().then((serviceAccount) => { + const request: HttpRequestConfig = { + method: 'POST', + url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signBlob`, + data: { payload: buffer.toString('base64') }, + }; + return this.httpClient.send(request); + }).then((response: any) => { + // Response from IAM is base64 encoded. Decode it into a buffer and return. + return Buffer.from(response.data.signedBlob, 'base64'); + }).catch((err) => { + if (err instanceof HttpError) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: err.message, + cause: err + }); + } + throw err + }); + } + + /** + * @inheritDoc + */ + public getAccountId(): Promise { + if (validator.isNonEmptyString(this.serviceAccountId)) { + return Promise.resolve(this.serviceAccountId); + } + const request: HttpRequestConfig = { + method: 'GET', + url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', + headers: { + 'Metadata-Flavor': 'Google', + }, + }; + const client = new HttpClient(); + return client.send(request).then((response) => { + if (!response.text) { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INTERNAL_ERROR, + message: 'HTTP Response missing payload', + }); + } + this.serviceAccountId = response.text; + return response.text; + }).catch((err) => { + throw new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_CREDENTIAL, + message: 'Failed to determine service account. Make sure to initialize ' + + 'the SDK with a service account credential. Alternatively specify a service ' + + `account with iam.serviceAccounts.signBlob permission. Original error: ${err}`, + }); + }); + } +} + +/** + * Creates a new CryptoSigner instance for the given app. If the app has been initialized with a + * service account credential, creates a ServiceAccountSigner. + * + * @param {FirebaseApp} app A FirebaseApp instance. + * @return {CryptoSigner} A CryptoSigner instance. + */ +export function cryptoSignerFromApp(app: FirebaseApp): CryptoSigner { + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return new ServiceAccountSigner(credential); + } + + return new IAMSigner(new AuthorizedHttpClient(app), app.options.serviceAccountId); +} + +/** + * Defines extended error info type. This includes a code, message string, and error data. + */ +export interface ExtendedErrorInfo extends ErrorInfo { + cause?: Error; +} + +/** + * CryptoSigner error code structure. + * + * @param {ErrorInfo} errorInfo The error information (code and message). + * @constructor + */ +export class CryptoSignerError extends Error { + constructor(private errorInfo: ExtendedErrorInfo) { + super(errorInfo.message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = CryptoSignerError.prototype; + } + + /** @return {string} The error code. */ + public get code(): string { + return this.errorInfo.code; + } + + /** @return {string} The error message. */ + public get message(): string { + return this.errorInfo.message; + } + + /** @return {object} The error data. */ + public get cause(): Error | undefined { + return this.errorInfo.cause; + } +} + +/** + * Crypto Signer error codes and their default messages. + */ +export class CryptoSignerErrorCode { + public static INVALID_ARGUMENT = 'invalid-argument'; + public static INTERNAL_ERROR = 'internal-error'; + public static INVALID_CREDENTIAL = 'invalid-credential'; + public static SERVER_ERROR = 'server-error'; +} diff --git a/src/utils/deep-copy.ts b/src/utils/deep-copy.ts index 72a791d30a..8e5bee9d8b 100644 --- a/src/utils/deep-copy.ts +++ b/src/utils/deep-copy.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/utils/error.ts b/src/utils/error.ts old mode 100755 new mode 100644 index 746cc3ca5d..fcec6d5fd3 --- 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 '../firebase-namespace-api'; +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). */ @@ -42,7 +39,7 @@ interface ServerToClientCode { * @param {ErrorInfo} errorInfo The error information (code and message). * @constructor */ -export class FirebaseError extends Error { +export class FirebaseError extends Error implements FirebaseErrorInterface { constructor(private errorInfo: ErrorInfo) { super(errorInfo.message); @@ -196,7 +193,7 @@ export class FirebaseAuthError extends PrefixedFirebaseError { export class FirebaseDatabaseError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'database/' + info.code, message: message || info.message}); + super({ code: 'database/' + info.code, message: message || info.message }); } } @@ -211,7 +208,7 @@ export class FirebaseDatabaseError extends FirebaseError { export class FirebaseFirestoreError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'firestore/' + info.code, message: message || info.message}); + super({ code: 'firestore/' + info.code, message: message || info.message }); } } @@ -226,7 +223,24 @@ export class FirebaseFirestoreError extends FirebaseError { export class FirebaseInstanceIdError extends FirebaseError { constructor(info: ErrorInfo, message?: string) { // Override default message if custom message provided. - super({code: 'instance-id/' + info.code, message: message || info.message}); + super({ code: 'instance-id/' + info.code, message: message || info.message }); + (this as any).__proto__ = FirebaseInstanceIdError.prototype; + } +} + +/** + * Firebase Installations service error code structure. This extends `FirebaseError`. + * + * @param info The error code info. + * @param message The error message. This will override the default + * message if provided. + * @constructor + */ +export class FirebaseInstallationsError extends FirebaseError { + constructor(info: ErrorInfo, message?: string) { + // Override default message if custom message provided. + super({ code: 'installations/' + info.code, message: message || info.message }); + (this as any).__proto__ = FirebaseInstallationsError.prototype; } } @@ -377,6 +391,10 @@ export class AuthClientErrorCode { code: 'email-already-exists', message: 'The email address is already in use by another account.', }; + public static EMAIL_NOT_FOUND = { + code: 'email-not-found', + message: 'There is no user record corresponding to the provided email.', + }; public static FORBIDDEN_CLAIM = { code: 'reserved-claim', message: 'The specified developer claim is reserved and cannot be specified.', @@ -524,6 +542,10 @@ export class AuthClientErrorCode { code: 'invalid-provider-uid', message: 'The providerUid must be a valid provider uid string.', }; + public static INVALID_OAUTH_RESPONSETYPE = { + code: 'invalid-oauth-responsetype', + message: 'Only exactly one OAuth responseType should be set to true.', + }; public static INVALID_SESSION_COOKIE_DURATION = { code: 'invalid-session-cookie-duration', message: 'The session cookie duration must be a valid number in milliseconds ' + @@ -537,6 +559,10 @@ export class AuthClientErrorCode { code: 'invalid-tenant-type', message: 'Tenant type must be either "full_service" or "lightweight".', }; + public static INVALID_TESTING_PHONE_NUMBER = { + code: 'invalid-testing-phone-number', + message: 'Invalid testing phone number or invalid test code provided.', + }; public static INVALID_UID = { code: 'invalid-uid', message: 'The uid must be a non-empty string with at most 128 characters.', @@ -592,6 +618,10 @@ export class AuthClientErrorCode { code: 'missing-oauth-client-id', message: 'The OAuth/OIDC configuration client ID must not be empty.', }; + public static MISSING_OAUTH_CLIENT_SECRET = { + code: 'missing-oauth-client-secret', + message: 'The OAuth configuration client secret is required to enable OIDC code flow.', + }; public static MISSING_PROVIDER_ID = { code: 'missing-provider-id', message: 'A valid provider ID must be provided in the request.', @@ -600,6 +630,10 @@ export class AuthClientErrorCode { code: 'missing-saml-relying-party-config', message: 'The SAML configuration provided is missing a relying party configuration.', }; + public static MAXIMUM_TEST_PHONE_NUMBER_EXCEEDED = { + code: 'test-phone-number-limit-exceeded', + message: 'The maximum allowed number of test phone number / code pairs has been exceeded.', + }; public static MAXIMUM_USER_COUNT_EXCEEDED = { code: 'maximum-user-count-exceeded', message: 'The maximum allowed number of users to import has been exceeded.', @@ -791,7 +825,7 @@ export class MessagingClientErrorCode { }; } -export class InstanceIdClientErrorCode { +export class InstallationsClientErrorCode { public static INVALID_ARGUMENT = { code: 'invalid-argument', message: 'Invalid argument provided.', @@ -800,13 +834,20 @@ export class InstanceIdClientErrorCode { code: 'invalid-project-id', message: 'Invalid project ID provided.', }; - public static INVALID_INSTANCE_ID = { - code: 'invalid-instance-id', - message: 'Invalid instance ID provided.', + public static INVALID_INSTALLATION_ID = { + code: 'invalid-installation-id', + message: 'Invalid installation ID provided.', }; public static API_ERROR = { code: 'api-error', - message: 'Instance ID API call failed.', + message: 'Installation ID API call failed.', + }; +} + +export class InstanceIdClientErrorCode extends InstallationsClientErrorCode { + public static INVALID_INSTANCE_ID = { + code: 'invalid-instance-id', + message: 'Invalid instance ID provided.', }; } @@ -849,6 +890,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { DUPLICATE_MFA_ENROLLMENT_ID: 'SECOND_FACTOR_UID_ALREADY_EXISTS', // setAccountInfo email already exists. EMAIL_EXISTS: 'EMAIL_ALREADY_EXISTS', + // /accounts:sendOobCode for password reset when user is not found. + EMAIL_NOT_FOUND: 'EMAIL_NOT_FOUND', // Reserved claim name. FORBIDDEN_CLAIM: 'FORBIDDEN_CLAIM', // Invalid claims provided. @@ -875,6 +918,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_PROVIDER_ID: 'INVALID_PROVIDER_ID', // Invalid service account. INVALID_SERVICE_ACCOUNT: 'INVALID_SERVICE_ACCOUNT', + // Invalid testing phone number. + INVALID_TESTING_PHONE_NUMBER: 'INVALID_TESTING_PHONE_NUMBER', // Invalid tenant type. INVALID_TENANT_TYPE: 'INVALID_TENANT_TYPE', // Missing Android package name. diff --git a/src/utils/index.ts b/src/utils/index.ts old mode 100755 new mode 100644 index f6e2f41232..d047397667 --- 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,11 +15,22 @@ * limitations under the License. */ -import {FirebaseApp, FirebaseAppOptions} from '../firebase-app'; -import {ServiceAccountCredential, ComputeEngineCredential} from '../auth/credential'; - +import { app as _app } from '../firebase-namespace-api'; +import { + ServiceAccountCredential, ComputeEngineCredential +} from '../credential/credential-internal'; import * as validator from './validator'; +let sdkVersion: string; + +export function getSdkVersion(): string { + if (!sdkVersion) { + const { version } = require('../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires + sdkVersion = version; + } + return sdkVersion; +} + /** * Renames properties on an object given a mapping from old to new property names. * @@ -60,12 +72,12 @@ export function addReadonlyGetter(obj: object, prop: string, value: any): void { * specified in either the Firebase app options, credentials or the local environment. * Otherwise returns null. * - * @param {FirebaseApp} app A Firebase app to get the project ID from. + * @param app A Firebase app to get the project ID from. * - * @return {string} A project ID string or null. + * @return A project ID string or null. */ -export function getExplicitProjectId(app: FirebaseApp): string | null { - const options: FirebaseAppOptions = app.options; +export function getExplicitProjectId(app: _app.App): string | null { + const options = app.options; if (validator.isNonEmptyString(options.projectId)) { return options.projectId; } @@ -89,11 +101,11 @@ export function getExplicitProjectId(app: FirebaseApp): string | null { * configured, but the SDK has been initialized with ComputeEngineCredentials, this * method attempts to discover the project ID from the local metadata service. * - * @param {FirebaseApp} app A Firebase app to get the project ID from. + * @param app A Firebase app to get the project ID from. * - * @return {Promise} A project ID string or null. + * @return A project ID string or null. */ -export function findProjectId(app: FirebaseApp): Promise { +export function findProjectId(app: _app.App): Promise { const projectId = getExplicitProjectId(app); if (projectId) { return Promise.resolve(projectId); @@ -142,25 +154,65 @@ export function formatString(str: string, params?: object): string { * Generates the update mask for the provided object. * Note this will ignore the last key with value undefined. * - * @param {[key: string]: any} obj The object to generate the update mask for. - * @return {Array} The computed update mask list. + * @param obj The object to generate the update mask for. + * @param terminalPaths The optional map of keys for maximum paths to traverse. + * Nested objects beyond that path will be ignored. This is useful for + * keys with variable object values. + * @param root The path so far. + * @return The computed update mask list. */ -export function generateUpdateMask(obj: {[key: string]: any}): string[] { +export function generateUpdateMask( + obj: any, terminalPaths: string[] = [], root = '' +): string[] { const updateMask: string[] = []; if (!validator.isNonNullObject(obj)) { return updateMask; } for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key) && typeof obj[key] !== 'undefined') { - const maskList = generateUpdateMask(obj[key]); - if (maskList.length > 0) { - maskList.forEach((mask) => { - updateMask.push(`${key}.${mask}`); - }); - } else { + if (typeof obj[key] !== 'undefined') { + const nextPath = root ? `${root}.${key}` : key; + // We hit maximum path. + // Consider switching to Set if the list grows too large. + if (terminalPaths.indexOf(nextPath) !== -1) { + // Add key and stop traversing this branch. updateMask.push(key); + } else { + const maskList = generateUpdateMask(obj[key], terminalPaths, nextPath); + if (maskList.length > 0) { + maskList.forEach((mask) => { + updateMask.push(`${key}.${mask}`); + }); + } else { + updateMask.push(key); + } } } } return updateMask; } + +/** + * Transforms milliseconds to a protobuf Duration type string. + * Returns the duration in seconds with up to nine fractional + * digits, terminated by 's'. Example: "3 seconds 0 nano seconds as 3s, + * 3 seconds 1 nano seconds as 3.000000001s". + * + * @param milliseconds The duration in milliseconds. + * @returns The resulting formatted string in seconds with up to nine fractional + * digits, terminated by 's'. + */ +export function transformMillisecondsToSecondsString(milliseconds: number): string { + let duration: string; + const seconds = Math.floor(milliseconds / 1000); + const nanos = Math.floor((milliseconds - seconds * 1000) * 1000000); + if (nanos > 0) { + let nanoString = nanos.toString(); + while (nanoString.length < 9) { + nanoString = '0' + nanoString; + } + duration = `${seconds}.${nanoString}s`; + } else { + duration = `${seconds}s`; + } + return duration; +} diff --git a/src/utils/jwt.ts b/src/utils/jwt.ts new file mode 100644 index 0000000000..1fab2ff9fb --- /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 ONE_DAY_IN_SECONDS = 24 * 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 cahced for 24 hours. + }); + } + + public fetchPublicKeys(): Promise<{ [key: string]: string }> { + if (this.shouldRefresh()) { + return this.refresh(); + } + return Promise.resolve(this.publicKeys); + } + + private shouldRefresh(): boolean { + return !this.publicKeys || this.publicKeysExpireAt <= Date.now(); + } + + private refresh(): Promise<{ [key: string]: string }> { + return this.client.getSigningKeys() + .then((signingKeys) => { + // reset expire at from previous set of keys. + this.publicKeysExpireAt = 0; + const newKeys = signingKeys.reduce((map: { [key: string]: string }, signingKey: jwks.SigningKey) => { + map[signingKey.kid] = signingKey.getPublicKey(); + return map; + }, {}); + this.publicKeysExpireAt = Date.now() + (ONE_DAY_IN_SECONDS * 1000); + this.publicKeys = newKeys; + return newKeys; + }).catch((err) => { + throw new Error(`Error fetching Json Web Keys: ${err.message}`); + }); + } +} + +/** + * Class to fetch public keys from a client certificates URL. + */ +export class UrlKeyFetcher implements KeyFetcher { + private publicKeys: { [key: string]: string }; + private publicKeysExpireAt = 0; + + constructor(private clientCertUrl: string, private readonly httpAgent?: Agent) { + if (!validator.isURL(clientCertUrl)) { + throw new Error( + 'The provided public client certificate URL is not a valid URL.', + ); + } + } + + /** + * Fetches the public keys for the Google certs. + * + * @return A promise fulfilled with public keys for the Google certs. + */ + public fetchPublicKeys(): Promise<{ [key: string]: string }> { + if (this.shouldRefresh()) { + return this.refresh(); + } + return Promise.resolve(this.publicKeys); + } + + /** + * Checks if the cached public keys need to be refreshed. + * + * @returns Whether the keys should be fetched from the client certs url or not. + */ + private shouldRefresh(): boolean { + return !this.publicKeys || this.publicKeysExpireAt <= Date.now(); + } + + private refresh(): Promise<{ [key: string]: string }> { + const client = new HttpClient(); + const request: HttpRequestConfig = { + method: 'GET', + url: this.clientCertUrl, + httpAgent: this.httpAgent, + }; + return client.send(request).then((resp) => { + if (!resp.isJson() || resp.data.error) { + // Treat all non-json messages and messages with an 'error' field as + // error responses. + throw new HttpError(resp); + } + // reset expire at from previous set of keys. + this.publicKeysExpireAt = 0; + if (Object.prototype.hasOwnProperty.call(resp.headers, 'cache-control')) { + const cacheControlHeader: string = resp.headers['cache-control']; + const parts = cacheControlHeader.split(','); + parts.forEach((part) => { + const subParts = part.trim().split('='); + if (subParts[0] === 'max-age') { + const maxAge: number = +subParts[1]; + this.publicKeysExpireAt = Date.now() + (maxAge * 1000); + } + }); + } + this.publicKeys = resp.data; + return resp.data; + }).catch((err) => { + if (err instanceof HttpError) { + let errorMessage = 'Error fetching public keys for Google certs: '; + const resp = err.response; + if (resp.isJson() && resp.data.error) { + errorMessage += `${resp.data.error}`; + if (resp.data.error_description) { + errorMessage += ' (' + resp.data.error_description + ')'; + } + } else { + errorMessage += `${resp.text}`; + } + throw new Error(errorMessage); + } + throw err; + }); + } +} + +/** + * Class for verifing JWT signature with a public key. + */ +export class PublicKeySignatureVerifier implements SignatureVerifier { + constructor(private keyFetcher: KeyFetcher) { + if (!validator.isNonNullObject(keyFetcher)) { + throw new Error('The provided key fetcher is not an object or null.'); + } + } + + public static withCertificateUrl(clientCertUrl: string, httpAgent?: Agent): PublicKeySignatureVerifier { + return new PublicKeySignatureVerifier(new UrlKeyFetcher(clientCertUrl, httpAgent)); + } + + public 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 verifing unsigned (emulator) JWTs. + */ +export class EmulatorSignatureVerifier implements SignatureVerifier { + public verify(token: string): Promise { + // Signature checks skipped for emulator; no need to fetch public keys. + return verifyJwtSignature(token, ''); + } +} + +/** + * Provides a callback to fetch public keys. + * + * @param fetcher KeyFetcher to fetch the keys from. + * @returns A callback function that can be used to get keys in `jsonwebtoken`. + */ +function getKeyCallback(fetcher: KeyFetcher): jwt.GetPublicKeyOrSecret { + return (header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) => { + 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 verfied. + * @param secretOrPublicKey The secret or a function to fetch the secret or public key. + * @param options JWT verification options. + * @returns A Promise resolving for a token with a valid signature. + */ +export function verifyJwtSignature(token: string, secretOrPublicKey: jwt.Secret | jwt.GetPublicKeyOrSecret, + options?: jwt.VerifyOptions): Promise { + if (!validator.isString(token)) { + return Promise.reject(new JwtError(JwtErrorCode.INVALID_ARGUMENT, + 'The provided token must be a string.')); + } + + return new Promise((resolve, reject) => { + jwt.verify(token, secretOrPublicKey, options, + (error: jwt.VerifyErrors | null) => { + if (!error) { + return resolve(); + } + if (error.name === 'TokenExpiredError') { + return reject(new JwtError(JwtErrorCode.TOKEN_EXPIRED, + 'The provided token has expired. Get a fresh token from your ' + + 'client app and try again.')); + } else if (error.name === 'JsonWebTokenError') { + if (error.message && error.message.includes(JWT_CALLBACK_ERROR_PREFIX)) { + const message = error.message.split(JWT_CALLBACK_ERROR_PREFIX).pop() || 'Error fetching public keys.'; + 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 old mode 100755 new mode 100644 index c2ce758267..bca0c660ab --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/integration/app-check.spec.ts b/test/integration/app-check.spec.ts new file mode 100644 index 0000000000..82ad153498 --- /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.have.keys(['iss', 'sub', 'aud', 'exp', 'iat', 'app_id']); + expect(verifedToken.token.app_id).to.be.a('string').and.equals(appId); + }); + }); + + it('should propagate API errors', () => { + // rejects with invalid-argument when the token is invalid + return admin.appCheck().verifyToken('invalid-token') + .should.eventually.be.rejected.and.have.property('code', 'app-check/invalid-argument'); + }); + }); +}); diff --git a/test/integration/app.spec.ts b/test/integration/app.spec.ts index 532adb5798..cce88a52aa 100644 --- a/test/integration/app.spec.ts +++ b/test/integration/app.spec.ts @@ -15,7 +15,7 @@ */ import * as admin from '../../lib/index'; -import {expect} from 'chai'; +import { expect } from 'chai'; import { defaultApp, nullApp, nonNullApp, databaseUrl, projectId, storageBucket, } from './setup'; @@ -27,6 +27,20 @@ describe('admin', () => { expect(storageBucket).to.be.not.empty; }); + it('does not load RTDB by default', () => { + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.be.undefined; + const rtdbInternal = require.cache[require.resolve('../../lib/database/database-internal')]; + expect(rtdbInternal).to.be.undefined; + }); + + it('loads RTDB when calling admin.database', () => { + const rtdbNamespace = admin.database; + expect(rtdbNamespace).to.not.be.null; + const firebaseRtdb = require.cache[require.resolve('@firebase/database')]; + expect(firebaseRtdb).to.not.be.undefined; + }); + it('does not load Firestore by default', () => { const gcloud = require.cache[require.resolve('@google-cloud/firestore')]; expect(gcloud).to.be.undefined; diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts old mode 100755 new mode 100644 index 4031618174..55b78fa232 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -19,16 +19,14 @@ import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as crypto from 'crypto'; import * as bcrypt from 'bcrypt'; -import * as scrypt from 'scrypt'; import firebase from '@firebase/app'; import '@firebase/auth'; -import {clone} from 'lodash'; +import { clone } from 'lodash'; import { generateRandomString, projectId, apiKey, noServiceAccountApp, cmdArgs, } from './setup'; import url = require('url'); import * as mocks from '../resources/mocks'; -import { AuthProviderConfig } from '../../src/auth/auth-config'; import { deepExtend, deepCopy } from '../../src/utils/deep-copy'; import { User, FirebaseAuth } from '@firebase/auth-types'; @@ -39,6 +37,8 @@ chai.use(chaiAsPromised); const expect = chai.expect; +const authEmulatorHost = process.env.FIREBASE_AUTH_EMULATOR_HOST; + const newUserUid = generateRandomString(20); const nonexistentUid = generateRandomString(20); const newMultiFactorUserUid = generateRandomString(20); @@ -104,6 +104,9 @@ describe('admin.auth', () => { apiKey, authDomain: projectId + '.firebaseapp.com', }); + if (authEmulatorHost) { + (clientAuth() as any).useEmulator('http://' + authEmulatorHost); + } return cleanup(); }); @@ -139,7 +142,10 @@ describe('admin.auth', () => { }); }); - it('createUser() creates a new user with enrolled second factors', () => { + it('createUser() creates a new user with enrolled second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } const enrolledFactors = [ { phoneNumber: '+16505550001', @@ -172,14 +178,16 @@ describe('admin.auth', () => { const firstMultiFactor = userRecord.multiFactor!.enrolledFactors[0]; expect(firstMultiFactor.uid).not.to.be.undefined; expect(firstMultiFactor.enrollmentTime).not.to.be.undefined; - expect((firstMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal(enrolledFactors[0].phoneNumber); + expect((firstMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal( + enrolledFactors[0].phoneNumber); expect(firstMultiFactor.displayName).to.equal(enrolledFactors[0].displayName); expect(firstMultiFactor.factorId).to.equal(enrolledFactors[0].factorId); // Confirm second enrolled second factor. const secondMultiFactor = userRecord.multiFactor!.enrolledFactors[1]; expect(secondMultiFactor.uid).not.to.be.undefined; expect(secondMultiFactor.enrollmentTime).not.to.be.undefined; - expect((secondMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal(enrolledFactors[1].phoneNumber); + expect((secondMultiFactor as admin.auth.PhoneMultiFactorInfo).phoneNumber).to.equal( + enrolledFactors[1].phoneNumber); expect(secondMultiFactor.displayName).to.equal(enrolledFactors[1].displayName); expect(secondMultiFactor.factorId).to.equal(enrolledFactors[1].factorId); }); @@ -213,13 +221,65 @@ describe('admin.auth', () => { }); }); + it('getUserByProviderUid() returns a user record with the matching provider id', async () => { + // TODO(rsgowman): Once we can link a provider id with a user, just do that + // here instead of creating a new user. + const randomUid = 'import_' + generateRandomString(20).toLowerCase(); + const importUser: admin.auth.UserImportRecord = { + uid: randomUid, + email: 'user@example.com', + phoneNumber: '+15555550000', + emailVerified: true, + disabled: false, + metadata: { + lastSignInTime: 'Thu, 01 Jan 1970 00:00:00 UTC', + creationTime: 'Thu, 01 Jan 1970 00:00:00 UTC', + }, + providerData: [{ + displayName: 'User Name', + email: 'user@example.com', + phoneNumber: '+15555550000', + photoURL: 'http://example.com/user', + providerId: 'google.com', + uid: 'google_uid', + }], + }; + + await admin.auth().importUsers([importUser]); + + try { + await admin.auth().getUserByProviderUid('google.com', 'google_uid') + .then((userRecord) => { + expect(userRecord.uid).to.equal(importUser.uid); + }); + } finally { + await safeDelete(importUser.uid); + } + }); + + it('getUserByProviderUid() redirects to getUserByEmail if given an email', () => { + return admin.auth().getUserByProviderUid('email', mockUserData.email) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + + it('getUserByProviderUid() redirects to getUserByPhoneNumber if given a phone number', () => { + return admin.auth().getUserByProviderUid('phone', mockUserData.phoneNumber) + .then((userRecord) => { + expect(userRecord.uid).to.equal(newUserUid); + }); + }); + describe('getUsers()', () => { /** * Filters a list of object to another list of objects that only contains * the uid, email, and phoneNumber fields. Works with at least UserRecord * and UserImportRecord instances. */ - function mapUserRecordsToUidEmailPhones(values: Array<{ uid: string; email?: string; phoneNumber?: string}>): Array<{ uid: string; email?: string; phoneNumber?: string}> { + function mapUserRecordsToUidEmailPhones( + values: Array<{ uid: string; email?: string; phoneNumber?: string}> + ): Array<{ uid: string; email?: string; phoneNumber?: string}> { return values.map((ur) => ({ uid: ur.uid, email: ur.email, phoneNumber: ur.phoneNumber })); } @@ -289,14 +349,14 @@ describe('admin.auth', () => { { uid: 'uid_that_doesnt_exist' }, { uid: 'uid3' }, ]); - expect(users.notFound).to.have.deep.members([{uid: 'uid_that_doesnt_exist'}]); + expect(users.notFound).to.have.deep.members([{ uid: 'uid_that_doesnt_exist' }]); const foundUsers = mapUserRecordsToUidEmailPhones(users.users); expect(foundUsers).to.have.deep.members([testUser1, testUser3]); }); it('returns nothing when queried for only non-existing users', async () => { - const notFoundIds = [{uid: 'non-existing user'}]; + const notFoundIds = [{ uid: 'non-existing user' }]; const users = await admin.auth().getUsers(notFoundIds); expect(users.users).to.be.empty; @@ -351,7 +411,7 @@ describe('admin.auth', () => { const metadata = userRecord!.metadata; expect(metadata.lastRefreshTime).to.exist; - expect(isUTCString(metadata.lastRefreshTime!)); + expect(isUTCString(metadata.lastRefreshTime!)).to.be.true; const creationTime = new Date(metadata.creationTime).getTime(); const lastRefreshTime = new Date(metadata.lastRefreshTime!).getTime(); expect(creationTime).lte(lastRefreshTime); @@ -408,12 +468,12 @@ describe('admin.auth', () => { }); }); - it('revokeRefreshTokens() invalidates existing sessions and ID tokens', () => { + it('revokeRefreshTokens() invalidates existing sessions and ID tokens', async () => { let currentIdToken: string; let currentUser: User; // Sign in with an email and password account. return clientAuth().signInWithEmailAndPassword(mockUserData.email, mockUserData.password) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; currentUser = user!; // Get user's ID token. @@ -431,9 +491,14 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - // verifyIdToken without checking revocation should still succeed. - return admin.auth().verifyIdToken(currentIdToken) - .should.eventually.be.fulfilled; + const verifyingIdToken = admin.auth().verifyIdToken(currentIdToken) + if (authEmulatorHost) { + // Check revocation is forced in emulator-mode and this should throw. + return verifyingIdToken.should.eventually.be.rejected; + } else { + // verifyIdToken without checking revocation should still succeed. + return verifyingIdToken.should.eventually.be.fulfilled; + } }) .then(() => { // verifyIdToken while checking for revocation should fail. @@ -450,7 +515,7 @@ describe('admin.auth', () => { return clientAuth().signInWithEmailAndPassword( mockUserData.email, mockUserData.password); }) - .then(({user}) => { + .then(({ user }) => { // Get new session's ID token. expect(user).to.exist; return user!.getIdToken(); @@ -475,7 +540,7 @@ describe('admin.auth', () => { return clientAuth().signInWithEmailAndPassword( userRecord.email!, mockUserData.password); }) - .then(({user}) => { + .then(({ user }) => { // Get the user's ID token. expect(user).to.exist; return user!.getIdToken(); @@ -518,68 +583,183 @@ describe('admin.auth', () => { }); }); - it('updateUser() updates the user record with the given parameters', () => { - const updatedDisplayName = 'Updated User ' + newUserUid; - const now = new Date(1476235905000).toUTCString(); - // Update user with enrolled second factors. - const enrolledFactors = [ - { - uid: 'mfaUid1', - phoneNumber: '+16505550001', - displayName: 'Work phone number', - factorId: 'phone', - enrollmentTime: now, - }, - { - uid: 'mfaUid2', - phoneNumber: '+16505550002', - displayName: 'Personal phone number', - factorId: 'phone', - enrollmentTime: now, - }, - ]; - return admin.auth().updateUser(newUserUid, { - email: updatedEmail, - phoneNumber: updatedPhone, - emailVerified: true, - displayName: updatedDisplayName, - multiFactor: { - enrolledFactors, - }, - }) - .then((userRecord) => { - expect(userRecord.emailVerified).to.be.true; - expect(userRecord.displayName).to.equal(updatedDisplayName); - // Confirm expected email. - expect(userRecord.email).to.equal(updatedEmail); - // Confirm expected phone number. - expect(userRecord.phoneNumber).to.equal(updatedPhone); - // Confirm second factors added to user. - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); - expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); - expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors); - // Update list of second factors. - return admin.auth().updateUser(newUserUid, { - multiFactor: { - enrolledFactors: [enrolledFactors[0]], - }, - }); + describe('updateUser()', () => { + /** + * Creates a new user for testing purposes. The user's uid will be + * '$name_$tenRandomChars' and email will be + * '$name_$tenRandomChars@example.com'. + */ + // TODO(rsgowman): This function could usefully be employed throughout this file. + function createTestUser(name: string): Promise { + const tenRandomChars = generateRandomString(10); + return admin.auth().createUser({ + uid: name + '_' + tenRandomChars, + displayName: name, + email: name + '_' + tenRandomChars + '@example.com', + }); + } + + let updateUser: admin.auth.UserRecord; + before(async () => { + updateUser = await createTestUser('UpdateUser'); + }); + + after(() => { + return safeDelete(updateUser.uid); + }); + + it('updates the user record with the given parameters', () => { + const updatedDisplayName = 'Updated User ' + updateUser.uid; + return admin.auth().updateUser(updateUser.uid, { + email: updatedEmail, + phoneNumber: updatedPhone, + emailVerified: true, + displayName: updatedDisplayName, }) - .then((userRecord) => { - expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(1); - const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); - expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]); - // Remove all second factors. - return admin.auth().updateUser(newUserUid, { - multiFactor: { - enrolledFactors: null, - }, + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + expect(userRecord.displayName).to.equal(updatedDisplayName); + // Confirm expected email. + expect(userRecord.email).to.equal(updatedEmail); + // Confirm expected phone number. + expect(userRecord.phoneNumber).to.equal(updatedPhone); }); + }); + + it('creates, updates, and removes second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + + const now = new Date(1476235905000).toUTCString(); + // Update user with enrolled second factors. + const enrolledFactors = [ + { + uid: 'mfaUid1', + phoneNumber: '+16505550001', + displayName: 'Work phone number', + factorId: 'phone', + enrollmentTime: now, + }, + { + uid: 'mfaUid2', + phoneNumber: '+16505550002', + displayName: 'Personal phone number', + factorId: 'phone', + enrollmentTime: now, + }, + ]; + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors, + }, }) - .then((userRecord) => { - // Confirm all second factors removed. - expect(userRecord.multiFactor).to.be.undefined; + .then((userRecord) => { + // Confirm second factors added to user. + const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + expect(actualUserRecord.multiFactor.enrolledFactors.length).to.equal(2); + expect(actualUserRecord.multiFactor.enrolledFactors).to.deep.equal(enrolledFactors); + // Update list of second factors. + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors: [enrolledFactors[0]], + }, + }); + }) + .then((userRecord) => { + expect(userRecord.multiFactor!.enrolledFactors.length).to.equal(1); + const actualUserRecord: {[key: string]: any} = userRecord.toJSON(); + expect(actualUserRecord.multiFactor.enrolledFactors[0]).to.deep.equal(enrolledFactors[0]); + // Remove all second factors. + return admin.auth().updateUser(updateUser.uid, { + multiFactor: { + enrolledFactors: null, + }, + }); + }) + .then((userRecord) => { + // Confirm all second factors removed. + expect(userRecord.multiFactor).to.be.undefined; + }); + }); + + it('can link/unlink with a federated provider', async function () { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + const googleFederatedUid = 'google_uid_' + generateRandomString(10); + let userRecord = await admin.auth().updateUser(updateUser.uid, { + providerToLink: { + providerId: 'google.com', + uid: googleFederatedUid, + }, + }); + + let providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + let providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.deep.include(googleFederatedUid); + expect(providerIds).to.deep.include('google.com'); + + userRecord = await admin.auth().updateUser(updateUser.uid, { + providersToUnlink: ['google.com'], + }); + + providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.not.deep.include(googleFederatedUid); + expect(providerIds).to.not.deep.include('google.com'); + }); + + it('can unlink multiple providers at once, incl a non-federated provider', async 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 admin.auth().updateUser(updateUser.uid, { + phoneNumber: '+15555550001', + providerToLink: { + providerId: 'google.com', + uid: googleFederatedUid, + }, }); + userRecord = await admin.auth().updateUser(updateUser.uid, { + providerToLink: { + providerId: 'facebook.com', + uid: facebookFederatedUid, + }, + }); + + let providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + let providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.deep.include.members([googleFederatedUid, facebookFederatedUid, '+15555550001']); + expect(providerIds).to.deep.include.members(['google.com', 'facebook.com', 'phone']); + + userRecord = await admin.auth().updateUser(updateUser.uid, { + providersToUnlink: ['google.com', 'facebook.com', 'phone'], + }); + + providerUids = userRecord.providerData.map((userInfo) => userInfo.uid); + providerIds = userRecord.providerData.map((userInfo) => userInfo.providerId); + expect(providerUids).to.not.deep.include.members([googleFederatedUid, facebookFederatedUid, '+15555550001']); + expect(providerIds).to.not.deep.include.members(['google.com', 'facebook.com', 'phone']); + }); + + it('noops successfully when given an empty providersToUnlink list', async () => { + const userRecord = await createTestUser('NoopWithEmptyProvidersToDeleteUser'); + try { + const updatedUserRecord = await admin.auth().updateUser(userRecord.uid, { + providersToUnlink: [], + }); + + expect(updatedUserRecord).to.deep.equal(userRecord); + } finally { + safeDelete(userRecord.uid); + } + }); }); it('getUser() fails when called with a non-existing UID', () => { @@ -597,6 +777,11 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); }); + it('getUserByProviderUid() fails when called with a non-existing provider id', () => { + return admin.auth().getUserByProviderUid('google.com', nonexistentUid) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + it('updateUser() fails when called with a non-existing UID', () => { return admin.auth().updateUser(nonexistentUid, { emailVerified: true, @@ -615,7 +800,7 @@ describe('admin.auth', () => { .then((customToken) => { return clientAuth().signInWithCustomToken(customToken); }) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -635,7 +820,7 @@ describe('admin.auth', () => { .then((customToken) => { return clientAuth().signInWithCustomToken(customToken); }) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -653,6 +838,49 @@ describe('admin.auth', () => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); + if (authEmulatorHost) { + describe('Auth emulator support', () => { + const uid = 'authEmulatorUser'; + before(() => { + return admin.auth().createUser({ + uid, + email: 'lastRefreshTimeUser@example.com', + password: 'p4ssword', + }); + }); + after(() => { + return admin.auth().deleteUser(uid); + }); + + it('verifyIdToken() succeeds when called with an unsigned token', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: uid, + }); + return admin.auth().verifyIdToken(unsignedToken); + }); + + it('verifyIdToken() fails when called with a token with wrong project', () => { + const unsignedToken = mocks.generateIdToken({ algorithm: 'none', audience: 'nosuch' }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + }); + + it('verifyIdToken() fails when called with a token that does not belong to a user', () => { + const unsignedToken = mocks.generateIdToken({ + algorithm: 'none', + audience: projectId, + issuer: 'https://securetoken.google.com/' + projectId, + subject: 'nosuch', + }); + return admin.auth().verifyIdToken(unsignedToken) + .should.eventually.be.rejected.and.have.property('code', 'auth/user-not-found'); + }); + }); + } + describe('Link operations', () => { const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; @@ -681,7 +909,7 @@ describe('admin.auth', () => { it('generatePasswordResetLink() should return a password reset link', () => { // Ensure old password set on created user. - return admin.auth().updateUser(uid, {password: 'password'}) + return admin.auth().updateUser(uid, { password: 'password' }) .then(() => { return admin.auth().generatePasswordResetLink(email, actionCodeSettings); }) @@ -703,7 +931,7 @@ describe('admin.auth', () => { it('generateEmailVerificationLink() should return a verification link', () => { // Ensure the user's email is unverified. - return admin.auth().updateUser(uid, {password: 'password', emailVerified: false}) + return admin.auth().updateUser(uid, { password: 'password', emailVerified: false }) .then((userRecord) => { expect(userRecord.emailVerified).to.be.false; return admin.auth().generateEmailVerificationLink(email, actionCodeSettings); @@ -746,6 +974,15 @@ describe('admin.auth', () => { enabled: true, passwordRequired: true, }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + // Add random phone number / code pairs. + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedCreatedTenant: any = { displayName: 'testTenant1', @@ -753,6 +990,15 @@ describe('admin.auth', () => { enabled: true, passwordRequired: true, }, + anonymousSignInEnabled: false, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, }; const expectedUpdatedTenant: any = { displayName: 'testTenantUpdated', @@ -760,6 +1006,14 @@ describe('admin.auth', () => { enabled: false, passwordRequired: true, }, + anonymousSignInEnabled: false, + multiFactorConfig: { + state: 'DISABLED', + factorIds: [], + }, + testPhoneNumbers: { + '+16505551234': '123456', + }, }; const expectedUpdatedTenant2: any = { displayName: 'testTenantUpdated', @@ -767,6 +1021,11 @@ describe('admin.auth', () => { enabled: true, passwordRequired: false, }, + anonymousSignInEnabled: false, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, }; // https://mochajs.org/ @@ -805,6 +1064,20 @@ describe('admin.auth', () => { }); }); + it('createTenant() can enable anonymous users', async () => { + const tenant = await admin.auth().tenantManager().createTenant({ + displayName: 'testTenantWithAnon', + emailSignInConfig: { + enabled: false, + passwordRequired: true, + }, + anonymousSignInEnabled: true, + }); + createdTenants.push(tenant.tenantId); + + expect(tenant.anonymousSignInEnabled).to.be.true; + }); + // Sanity check user management + email link generation + custom attribute APIs. // TODO: Confirm behavior in client SDK when it starts supporting it. describe('supports user management, email link generation, custom attribute and token revocation APIs', () => { @@ -973,7 +1246,7 @@ describe('admin.auth', () => { clientAuth().tenantId = createdTenantId; const customToken = await tenantAwareAuth.createCustomToken('uid1'); - const {user} = await clientAuth().signInWithCustomToken(customToken); + const { user } = await clientAuth().signInWithCustomToken(customToken); expect(user).to.not.be.null; const idToken = await user!.getIdToken(); const token = await tenantAwareAuth.verifyIdToken(idToken); @@ -1042,7 +1315,7 @@ describe('admin.auth', () => { }) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); }) @@ -1061,12 +1334,31 @@ describe('admin.auth', () => { 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() { @@ -1096,12 +1388,10 @@ describe('admin.auth', () => { .then((config) => { assertDeepEqualUnordered(authProviderConfig, config); return tenantAwareAuth.updateProviderConfig( - authProviderConfig.providerId, modifiedConfigOptions); + authProviderConfig.providerId, deltaChanges); }) .then((config) => { - const modifiedConfig = deepExtend( - {providerId: authProviderConfig.providerId}, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); + assertDeepEqualUnordered(modifiedConfigOptions, config); return tenantAwareAuth.deleteProviderConfig(authProviderConfig.providerId); }) .then(() => { @@ -1126,12 +1416,17 @@ describe('admin.auth', () => { emailSignInConfig: { enabled: false, }, + multiFactorConfig: deepCopy(expectedUpdatedTenant.multiFactorConfig), + testPhoneNumbers: deepCopy(expectedUpdatedTenant.testPhoneNumbers), }; const updatedOptions2: admin.auth.UpdateTenantRequest = { emailSignInConfig: { enabled: true, passwordRequired: false, }, + multiFactorConfig: deepCopy(expectedUpdatedTenant2.multiFactorConfig), + // Test clearing of phone numbers. + testPhoneNumbers: null, }; return admin.auth().tenantManager().updateTenant(createdTenantId, updatedOptions) .then((actualTenant) => { @@ -1143,6 +1438,25 @@ describe('admin.auth', () => { }); }); + it('updateTenant() should be able to enable/disable anon provider', async () => { + const tenantManager = admin.auth().tenantManager(); + let tenant = await tenantManager.createTenant({ + displayName: 'testTenantUpdateAnon', + }); + createdTenants.push(tenant.tenantId); + expect(tenant.anonymousSignInEnabled).to.be.false; + + tenant = await tenantManager.updateTenant(tenant.tenantId, { + anonymousSignInEnabled: true, + }); + expect(tenant.anonymousSignInEnabled).to.be.true; + + tenant = await tenantManager.updateTenant(tenant.tenantId, { + anonymousSignInEnabled: false, + }); + expect(tenant.anonymousSignInEnabled).to.be.false; + }); + it('listTenants() should resolve with expected number of tenants', () => { const allTenantIds: string[] = []; const tenantOptions2 = deepCopy(tenantOptions); @@ -1218,7 +1532,10 @@ describe('admin.auth', () => { }; // Clean up temp configurations used for test. - before(() => { + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } return removeTempConfigs().then(() => admin.auth().createProviderConfig(authProviderConfig1)); }); @@ -1241,11 +1558,11 @@ describe('admin.auth', () => { }); it('listProviderConfig() successfully returns the list of SAML providers', () => { - const configs: AuthProviderConfig[] = []; + const configs: admin.auth.AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { - return admin.auth().listProviderConfigs({type, maxResults, pageToken}) + return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { - result.providerConfigs.forEach((config: AuthProviderConfig) => { + result.providerConfigs.forEach((config: admin.auth.AuthProviderConfig) => { configs.push(config); }); if (result.pageToken) { @@ -1285,7 +1602,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1313,7 +1630,7 @@ describe('admin.auth', () => { return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) .then((config) => { const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); + { providerId: authProviderConfig1.providerId }, modifiedConfigOptions); assertDeepEqualUnordered(modifiedConfig, config); }); }); @@ -1333,6 +1650,9 @@ describe('admin.auth', () => { enabled: true, issuer: 'https://oidc.com/issuer1', clientId: 'CLIENT_ID1', + responseType: { + idToken: true, + }, }; const authProviderConfig2 = { providerId: randomOidcProviderId(), @@ -1340,6 +1660,10 @@ describe('admin.auth', () => { enabled: true, issuer: 'https://oidc.com/issuer2', clientId: 'CLIENT_ID2', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; const removeTempConfigs = (): Promise => { @@ -1350,7 +1674,10 @@ describe('admin.auth', () => { }; // Clean up temp configurations used for test. - before(() => { + before(function () { + if (authEmulatorHost) { + return this.skip(); // Not implemented. + } return removeTempConfigs().then(() => admin.auth().createProviderConfig(authProviderConfig1)); }); @@ -1373,11 +1700,11 @@ describe('admin.auth', () => { }); it('listProviderConfig() successfully returns the list of OIDC providers', () => { - const configs: AuthProviderConfig[] = []; + const configs: admin.auth.AuthProviderConfig[] = []; const listProviders: any = (type: 'saml' | 'oidc', maxResults?: number, pageToken?: string) => { - return admin.auth().listProviderConfigs({type, maxResults, pageToken}) + return admin.auth().listProviderConfigs({ type, maxResults, pageToken }) .then((result) => { - result.providerConfigs.forEach((config: AuthProviderConfig) => { + result.providerConfigs.forEach((config: admin.auth.AuthProviderConfig) => { configs.push(config); }); if (result.pageToken) { @@ -1403,39 +1730,65 @@ describe('admin.auth', () => { }); }); - it('updateProviderConfig() successfully overwrites an OIDC config', () => { + it('updateProviderConfig() successfully partially modifies an OIDC config', () => { + const deltaChanges = { + displayName: 'OIDC_DISPLAY_NAME3', + enabled: false, + issuer: 'https://oidc.com/issuer3', + clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, + }; + // Only above fields should be modified. const modifiedConfigOptions = { + providerId: authProviderConfig1.providerId, displayName: 'OIDC_DISPLAY_NAME3', enabled: false, issuer: 'https://oidc.com/issuer3', clientId: 'CLIENT_ID3', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; - return admin.auth().updateProviderConfig(authProviderConfig1.providerId, modifiedConfigOptions) + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) .then((config) => { - const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); + assertDeepEqualUnordered(modifiedConfigOptions, config); }); }); - it('updateProviderConfig() successfully partially modifies an OIDC config', () => { + it('updateProviderConfig() with invalid oauth response type should be rejected', () => { const deltaChanges = { displayName: 'OIDC_DISPLAY_NAME4', + enabled: false, issuer: 'https://oidc.com/issuer4', + clientId: 'CLIENT_ID4', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: false, + }, }; - // Only above fields should be modified. - const modifiedConfigOptions = { - displayName: 'OIDC_DISPLAY_NAME4', + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges). + should.eventually.be.rejected.and.have.property('code', 'auth/invalid-oauth-responsetype'); + }); + + it('updateProviderConfig() code flow with no client secret should be rejected', () => { + const deltaChanges = { + displayName: 'OIDC_DISPLAY_NAME5', enabled: false, - issuer: 'https://oidc.com/issuer4', - clientId: 'CLIENT_ID3', + issuer: 'https://oidc.com/issuer5', + clientId: 'CLIENT_ID5', + responseType: { + idToken: false, + code: true, + }, }; - return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges) - .then((config) => { - const modifiedConfig = deepExtend( - {providerId: authProviderConfig1.providerId}, modifiedConfigOptions); - assertDeepEqualUnordered(modifiedConfig, config); - }); + return admin.auth().updateProviderConfig(authProviderConfig1.providerId, deltaChanges). + should.eventually.be.rejected.and.have.property('code', 'auth/missing-oauth-client-secret'); }); it('deleteProviderConfig() successfully deletes an existing OIDC config', () => { @@ -1449,7 +1802,6 @@ describe('admin.auth', () => { it('deleteUser() deletes the user with the given UID', () => { return Promise.all([ admin.auth().deleteUser(newUserUid), - admin.auth().deleteUser(newMultiFactorUserUid), admin.auth().deleteUser(uidFromCreateUserWithoutUid), ]).should.eventually.be.fulfilled; }); @@ -1459,7 +1811,7 @@ describe('admin.auth', () => { const uid1 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid2 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid3 = await admin.auth().createUser({}).then((ur) => ur.uid); - const ids = [{uid: uid1}, {uid: uid2}, {uid: uid3}]; + const ids = [{ uid: uid1 }, { uid: uid2 }, { uid: uid3 }]; return deleteUsersWithDelay([uid1, uid2, uid3]) .then((deleteUsersResult) => { @@ -1478,7 +1830,7 @@ describe('admin.auth', () => { it('deletes users that exist even when non-existing users also specified', async () => { const uid1 = await admin.auth().createUser({}).then((ur) => ur.uid); const uid2 = 'uid-that-doesnt-exist'; - const ids = [{uid: uid1}, {uid: uid2}]; + const ids = [{ uid: uid1 }, { uid: uid2 }]; return deleteUsersWithDelay([uid1, uid2]) .then((deleteUsersResult) => { @@ -1522,9 +1874,9 @@ describe('admin.auth', () => { const uid3 = sessionCookieUids[2]; it('creates a valid Firebase session cookie', () => { - return admin.auth().createCustomToken(uid, {admin: true, groupId: '1234'}) + return admin.auth().createCustomToken(uid, { admin: true, groupId: '1234' }) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1540,7 +1892,7 @@ describe('admin.auth', () => { delete payloadClaims.iat; expectedIat = Math.floor(new Date().getTime() / 1000); // One day long session cookie. - return admin.auth().createSessionCookie(currentIdToken, {expiresIn}); + return admin.auth().createSessionCookie(currentIdToken, { expiresIn }); }) .then((sessionCookie) => admin.auth().verifySessionCookie(sessionCookie)) .then((decodedIdToken) => { @@ -1560,13 +1912,13 @@ describe('admin.auth', () => { let currentSessionCookie: string; return admin.auth().createCustomToken(uid2) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) .then((idToken) => { // One day long session cookie. - return admin.auth().createSessionCookie(idToken, {expiresIn}); + return admin.auth().createSessionCookie(idToken, { expiresIn }); }) .then((sessionCookie) => { currentSessionCookie = sessionCookie; @@ -1575,8 +1927,14 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - return admin.auth().verifySessionCookie(currentSessionCookie) - .should.eventually.be.fulfilled; + const verifyingSessionCookie = admin.auth().verifySessionCookie(currentSessionCookie); + if (authEmulatorHost) { + // Check revocation is forced in emulator-mode and this should throw. + return verifyingSessionCookie.should.eventually.be.rejected; + } else { + // verifyIdToken without checking revocation should still succeed. + return verifyingSessionCookie.should.eventually.be.fulfilled; + } }) .then(() => { return admin.auth().verifySessionCookie(currentSessionCookie, true) @@ -1585,9 +1943,9 @@ describe('admin.auth', () => { }); it('fails when called with a revoked ID token', () => { - return admin.auth().createCustomToken(uid3, {admin: true, groupId: '1234'}) + return admin.auth().createCustomToken(uid3, { admin: true, groupId: '1234' }) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1598,7 +1956,7 @@ describe('admin.auth', () => { ), 1000)); }) .then(() => { - return admin.auth().createSessionCookie(currentIdToken, {expiresIn}) + return admin.auth().createSessionCookie(currentIdToken, { expiresIn }) .should.eventually.be.rejected.and.have.property('code', 'auth/id-token-expired'); }); }); @@ -1615,7 +1973,7 @@ describe('admin.auth', () => { it('fails when called with a Firebase ID token', () => { return admin.auth().createCustomToken(uid) .then((customToken) => clientAuth().signInWithCustomToken(customToken)) - .then(({user}) => { + .then(({ user }) => { expect(user).to.exist; return user!.getIdToken(); }) @@ -1745,8 +2103,14 @@ describe('admin.auth', () => { expect(userImportTest.importOptions.hash.derivedKeyLength).to.exist; const dkLen = userImportTest.importOptions.hash.derivedKeyLength!; - return Buffer.from(scrypt.hashSync( - currentRawPassword, {N, r, p}, dkLen, Buffer.from(currentRawSalt))); + return Buffer.from( + crypto.scryptSync( + currentRawPassword, + Buffer.from(currentRawSalt), + dkLen, + { + N, r, p, + })); }, rawPassword, rawSalt, @@ -1783,7 +2147,10 @@ describe('admin.auth', () => { ]; fixtures.forEach((fixture) => { - it(`successfully imports users with ${fixture.name} to Firebase Auth.`, () => { + it(`successfully imports users with ${fixture.name} to Firebase Auth.`, function () { + if (authEmulatorHost) { + return this.skip(); // Auth Emulator does not support real hashes. + } importUserRecord = { uid: randomUid, email: randomUid + '@example.com', @@ -1812,7 +2179,7 @@ describe('admin.auth', () => { photoURL, phoneNumber: '+15554446666', disabled: false, - customClaims: {admin: true}, + customClaims: { admin: true }, metadata: { lastSignInTime: now, creationTime: now, @@ -1852,10 +2219,13 @@ describe('admin.auth', () => { expect(JSON.stringify(actualUserRecord[key])) .to.be.equal(JSON.stringify((importUserRecord as any)[key])); } - }).should.eventually.be.fulfilled; + }); }); - it('successfully imports users with enrolled second factors', () => { + it('successfully imports users with enrolled second factors', function () { + if (authEmulatorHost) { + return this.skip(); // Not yet implemented. + } const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; const now = new Date(1476235905000).toUTCString(); @@ -1917,25 +2287,41 @@ describe('admin.auth', () => { it('fails when invalid users are provided', () => { const users = [ - {uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error'}, - {uid: generateRandomString(20).toLowerCase(), email: 'invalid'}, - {uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid'}, - {uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid'} as any, + { uid: generateRandomString(20).toLowerCase(), email: 'invalid' }, + { uid: generateRandomString(20).toLowerCase(), emailVerified: 'invalid' } as any, ]; return admin.auth().importUsers(users) .then((result) => { expect(result.successCount).to.equal(0); - expect(result.failureCount).to.equal(4); - expect(result.errors.length).to.equal(4); + expect(result.failureCount).to.equal(2); + expect(result.errors.length).to.equal(2); + expect(result.errors[0].index).to.equal(0); + expect(result.errors[0].error.code).to.equals('auth/invalid-email'); + expect(result.errors[1].index).to.equal(1); + expect(result.errors[1].error.code).to.equals('auth/invalid-email-verified'); + }); + }); + + it('fails when users with invalid phone numbers are provided', function () { + if (authEmulatorHost) { + // Auth Emulator's phoneNumber validation is also lax and won't throw. + return this.skip(); + } + const users = [ + // These phoneNumbers passes local (lax) validator but fails remotely. + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1error' }, + { uid: generateRandomString(20).toLowerCase(), phoneNumber: '+1invalid' }, + ]; + return admin.auth().importUsers(users) + .then((result) => { + expect(result.successCount).to.equal(0); + expect(result.failureCount).to.equal(2); + expect(result.errors.length).to.equal(2); expect(result.errors[0].index).to.equal(0); expect(result.errors[0].error.code).to.equals('auth/invalid-user-import'); expect(result.errors[1].index).to.equal(1); - expect(result.errors[1].error.code).to.equals('auth/invalid-email'); - expect(result.errors[2].index).to.equal(2); - expect(result.errors[2].error.code).to.equals('auth/invalid-user-import'); - expect(result.errors[3].index).to.equal(3); - expect(result.errors[3].error.code).to.equals('auth/invalid-email-verified'); - }).should.eventually.be.fulfilled; + expect(result.errors[1].error.code).to.equals('auth/invalid-user-import'); + }); }); }); }); @@ -1964,7 +2350,7 @@ function testImportAndSignInUser( // Sign in with an email and password to the imported account. return clientAuth().signInWithEmailAndPassword(users[0].email!, rawPassword); }) - .then(({user}) => { + .then(({ user }) => { // Confirm successful sign-in. expect(user).to.exist; expect(user!.email).to.equal(users[0].email); @@ -1976,8 +2362,8 @@ function testImportAndSignInUser( /** * Helper function that deletes the user with the specified phone number * if it exists. - * @param {string} phoneNumber The phone number of the user to delete. - * @return {Promise} A promise that resolves when the user is deleted + * @param phoneNumber The phone number of the user to delete. + * @return A promise that resolves when the user is deleted * or is found not to exist. */ function deletePhoneNumberUser(phoneNumber: string): Promise { @@ -2091,12 +2477,11 @@ function safeDelete(uid: string): Promise { * @param {string[]} uids The list of user identifiers to delete. * @return {Promise} A promise that resolves when delete operation resolves. */ -function deleteUsersWithDelay(uids: string[]): Promise { - return new Promise((resolve) => { - setTimeout(resolve, 1000); - }).then(() => { - return admin.auth().deleteUsers(uids); - }); +async function deleteUsersWithDelay(uids: string[]): Promise { + if (!authEmulatorHost) { + await new Promise((resolve) => { setTimeout(resolve, 1000); }); + } + return admin.auth().deleteUsers(uids); } /** diff --git a/test/integration/database.spec.ts b/test/integration/database.spec.ts index b64e099813..77204a1839 100644 --- a/test/integration/database.spec.ts +++ b/test/integration/database.spec.ts @@ -17,7 +17,7 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl} from './setup'; +import { defaultApp, nullApp, nonNullApp, cmdArgs, databaseUrl, isEmulator } from './setup'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); @@ -64,7 +64,13 @@ describe('admin.database', () => { .should.eventually.be.fulfilled; }); - it('App with null auth overrides is blocked by security rules', () => { + it('App with null auth overrides is blocked by security rules', function () { + if (isEmulator) { + // RTDB emulator has open security rules by default and won't block this. + // TODO(https://github.com/firebase/firebase-admin-node/issues/1149): + // remove this once updating security rules through admin is in place. + return this.skip(); + } return nullApp.database().ref('blocked').set(admin.database.ServerValue.TIMESTAMP) .should.eventually.be.rejectedWith('PERMISSION_DENIED: Permission denied'); }); @@ -157,13 +163,21 @@ describe('admin.database', () => { }); }); - it('admin.database().getRules() returns currently defined rules as a string', () => { + it('admin.database().getRules() returns currently defined rules as a string', function () { + if (isEmulator) { + // https://github.com/firebase/firebase-admin-node/issues/1149 + return this.skip(); + } return admin.database().getRules().then((result) => { return expect(result).to.be.not.empty; }); }); - it('admin.database().getRulesJSON() returns currently defined rules as an object', () => { + it('admin.database().getRulesJSON() returns currently defined rules as an object', function () { + if (isEmulator) { + // https://github.com/firebase/firebase-admin-node/issues/1149 + return this.skip(); + } return admin.database().getRulesJSON().then((result) => { return expect(result).to.be.not.undefined; }); @@ -173,8 +187,6 @@ describe('admin.database', () => { // Check for type compilation. This method is not invoked by any tests. But it // will trigger a TS compilation failure if the RTDB typings were not loaded // correctly. (Marked as export to avoid compilation warning.) -// -// eslint-disable-next-line @typescript-eslint/no-unused-vars export function addValueEventListener( db: admin.database.Database, callback: (s: admin.database.DataSnapshot | null) => any): void { diff --git a/test/integration/firestore.spec.ts b/test/integration/firestore.spec.ts index d3bbf51493..13ef9131dd 100644 --- a/test/integration/firestore.spec.ts +++ b/test/integration/firestore.spec.ts @@ -17,7 +17,7 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {clone} from 'lodash'; +import { clone } from 'lodash'; chai.should(); chai.use(chaiAsPromised); @@ -113,12 +113,44 @@ describe('admin.firestore', () => { expect(typeof admin.firestore.WriteResult).to.be.not.undefined; }); + it('admin.firestore.GrpcStatus type is defined', () => { + expect(typeof admin.firestore.GrpcStatus).to.be.not.undefined; + }); + + it('supports operations with custom type converters', () => { + const converter: admin.firestore.FirestoreDataConverter = { + toFirestore: (city: City) => { + return { + name: city.localId, + population: city.people, + }; + }, + fromFirestore: (snap: admin.firestore.QueryDocumentSnapshot) => { + return new City(snap.data().name, snap.data().population); + } + }; + + const expected: City = new City('Sunnyvale', 153185); + const refWithConverter: admin.firestore.DocumentReference = admin.firestore() + .collection('cities') + .doc() + .withConverter(converter); + return refWithConverter.set(expected) + .then(() => { + return refWithConverter.get(); + }) + .then((snapshot: admin.firestore.DocumentSnapshot) => { + expect(snapshot.data()).to.be.instanceOf(City); + return refWithConverter.delete(); + }); + }); + it('supports saving references in documents', () => { const source = admin.firestore().collection('cities').doc(); const target = admin.firestore().collection('cities').doc(); return source.set(mountainView) .then(() => { - return target.set({name: 'Palo Alto', sisterCity: source}); + return target.set({ name: 'Palo Alto', sisterCity: source }); }) .then(() => { return target.get(); @@ -141,7 +173,7 @@ describe('admin.firestore', () => { admin.firestore.setLogFunction((log) => { logs.push(log); }); - return source.set({name: 'San Francisco'}) + return source.set({ name: 'San Francisco' }) .then(() => { return source.delete(); }) @@ -150,3 +182,7 @@ describe('admin.firestore', () => { }); }); }); + +class City { + constructor(readonly localId: string, readonly people: number) { } +} diff --git a/test/integration/installations.spec.ts b/test/integration/installations.spec.ts new file mode 100644 index 0000000000..982bd3a218 --- /dev/null +++ b/test/integration/installations.spec.ts @@ -0,0 +1,31 @@ +/*! + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as admin from '../../lib/index'; +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +chai.should(); +chai.use(chaiAsPromised); + +describe('admin.installations', () => { + it('deleteInstallation() fails when called with fictive-ID0 instance ID', () => { + // instance ids have to conform to /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ + return admin.installations().deleteInstallation('fictive-ID0') + .should.eventually.be + .rejectedWith('Installation ID "fictive-ID0": Failed to find the installation ID.'); + }); +}); diff --git a/test/integration/instance-id.spec.ts b/test/integration/instance-id.spec.ts index a7d6eb6df2..d98c2802f6 100644 --- a/test/integration/instance-id.spec.ts +++ b/test/integration/instance-id.spec.ts @@ -26,6 +26,6 @@ describe('admin.instanceId', () => { // instance ids have to conform to /[cdef][A-Za-z0-9_-]{9}[AEIMQUYcgkosw048]/ return admin.instanceId().deleteInstanceId('fictive-ID0') .should.eventually.be - .rejectedWith('Instance ID "fictive-ID0": Failed to find the instance ID.'); + .rejectedWith('Installation ID "fictive-ID0": Failed to find the installation ID.'); }); }); diff --git a/test/integration/machine-learning.spec.ts b/test/integration/machine-learning.spec.ts index 9cef0d31db..f767b7d0ad 100644 --- a/test/integration/machine-learning.spec.ts +++ b/test/integration/machine-learning.spec.ts @@ -18,7 +18,11 @@ import path = require('path'); import * as chai from 'chai'; import * as admin from '../../lib/index'; -import {Bucket} from '@google-cloud/storage'; +import { projectId } from './setup'; +import { Bucket } from '@google-cloud/storage'; + +import AutoMLTfliteModelOptions = admin.machineLearning.AutoMLTfliteModelOptions; +import GcsTfliteModelOptions = admin.machineLearning.GcsTfliteModelOptions; const expect = chai.expect; @@ -61,7 +65,7 @@ describe('admin.machineLearning', () => { function uploadModelToGcs(localFileName: string, gcsFileName: string): Promise { const bucket: Bucket = admin.storage().bucket(); const tfliteFileName = path.join(__dirname, `../resources/${localFileName}`); - return bucket.upload(tfliteFileName, {destination: gcsFileName}) + return bucket.upload(tfliteFileName, { destination: gcsFileName }) .then(() => { return `gs://${bucket.name}/${gcsFileName}`; }); @@ -74,8 +78,9 @@ describe('admin.machineLearning', () => { describe('createModel()', () => { it('creates a new Model without ModelFormat', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-1', - tags: ['tag123', 'tag345']}; + displayName: 'node-integ-test-create-1', + tags: ['tag123', 'tag345'] + }; return admin.machineLearning().createModel(modelOptions) .then((model) => { scheduleForDelete(model); @@ -83,11 +88,11 @@ describe('admin.machineLearning', () => { }); }); - it('creates a new Model with valid ModelFormat', () => { + it('creates a new Model with valid GCS TFLite ModelFormat', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-2', + displayName: 'node-integ-test-create-2', tags: ['tag234', 'tag456'], - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('model1.tflite', 'valid_model.tflite') .then((fileName: string) => { @@ -100,12 +105,37 @@ describe('admin.machineLearning', () => { }); }); + it('creates a new Model with valid AutoML TFLite ModelFormat', function () { + // AutoML models require verification. This takes between 20 and 60 seconds + this.timeout(60000); // Allow up to 60 seconds for this test. + return getAutoMLModelReference() + .then((automlRef: string) => { + if (!automlRef) { + this.skip(); + return; + } + const modelOptions: admin.machineLearning.ModelOptions = { + displayName: 'node-integ-test-create-automl', + tags: ['tagAutoml'], + tfliteModel: { automlModel: automlRef } + }; + return admin.machineLearning().createModel(modelOptions) + .then((model) => { + return model.waitForUnlocked(55000) + .then(() => { + scheduleForDelete(model); + verifyModel(model, modelOptions); + }); + }); + }); + }); + it('creates a new Model with invalid ModelFormat', () => { // Upload a file to default gcs bucket const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-create-3', + displayName: 'node-integ-test-create-3', tags: ['tag234', 'tag456'], - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + tfliteModel: { gcsTfliteUri: 'this will be replaced below' }, }; return uploadModelToGcs('invalid_model.tflite', 'invalid_model.tflite') .then((fileName: string) => { @@ -150,15 +180,15 @@ describe('admin.machineLearning', () => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: 'Invalid Name#*^!', }; - return createTemporaryModel({displayName: 'node-integration-invalid-arg'}) + return createTemporaryModel({ displayName: 'node-integ-invalid-argument' }) .then((model) => admin.machineLearning().updateModel(model.modelId, modelOptions) .should.eventually.be.rejected.and.have.property( 'code', 'machine-learning/invalid-argument')); }); it('updates the displayName', () => { - const DISPLAY_NAME = 'node-integration-test-update-1b'; - return createTemporaryModel({displayName: 'node-integration-test-update-1a'}) + const DISPLAY_NAME = 'node-integ-test-update-1b'; + return createTemporaryModel({ displayName: 'node-integ-test-update-1a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -175,7 +205,7 @@ describe('admin.machineLearning', () => { const NEW_TAGS = ['tag-node-update-2', 'tag-node-update-3']; return createTemporaryModel({ - displayName: 'node-integration-test-update-2', + displayName: 'node-integ-test-update-2', tags: ORIGINAL_TAGS, }).then((expectedModel) => { const modelOptions: admin.machineLearning.ModelOptions = { @@ -195,7 +225,7 @@ describe('admin.machineLearning', () => { uploadModelToGcs('model1.tflite', 'valid_model.tflite')]) .then(([model, fileName]) => { const modelOptions: admin.machineLearning.ModelOptions = { - tfliteModel: {gcsTfliteUri: fileName}, + tfliteModel: { gcsTfliteUri: fileName }, }; return admin.machineLearning().updateModel(model.modelId, modelOptions) .then((updatedModel) => { @@ -204,10 +234,37 @@ describe('admin.machineLearning', () => { }); }); + it('updates the automl model', function () { + // AutoML models require verification. This takes between 20 and 60 seconds + this.timeout(60000); // Allow up to 60 seconds for this test. + return createTemporaryModel({ + displayName: 'node-integ-test-update-automl' + }).then((model) => { + + return getAutoMLModelReference() + .then((automlRef: string) => { + if (!automlRef) { + this.skip(); + return; + } + const modelOptions: admin.machineLearning.ModelOptions = { + tfliteModel: { automlModel: automlRef }, + }; + return admin.machineLearning().updateModel(model.modelId, modelOptions) + .then((updatedModel) => { + return updatedModel.waitForUnlocked(55000) + .then(() => { + verifyModel(updatedModel, modelOptions); + }); + }); + }); + }); + }); + it('can update more than 1 field', () => { - const DISPLAY_NAME = 'node-integration-test-update-3b'; - const TAGS = ['node-integration-tag-1', 'node-integration-tag-2']; - return createTemporaryModel({displayName: 'node-integration-test-update-3a'}) + const DISPLAY_NAME = 'node-integ-test-update-3b'; + const TAGS = ['node-integ-tag-1', 'node-integ-tag-2']; + return createTemporaryModel({ displayName: 'node-integ-test-update-3a' }) .then((model) => { const modelOptions: admin.machineLearning.ModelOptions = { displayName: DISPLAY_NAME, @@ -238,15 +295,15 @@ describe('admin.machineLearning', () => { it('publishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-publish-1', - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + 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.empty; + expect(createdModel.validationError).to.be.undefined; expect(createdModel.published).to.be.false; return admin.machineLearning().publishModel(createdModel.modelId) .then((publishedModel) => { @@ -273,15 +330,15 @@ describe('admin.machineLearning', () => { it('unpublishes the model successfully', () => { const modelOptions: admin.machineLearning.ModelOptions = { - displayName: 'node-integration-test-unpublish1', - tfliteModel: {gcsTfliteUri: 'this will be replaced below'}, + 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.empty; + expect(createdModel.validationError).to.be.undefined; expect(createdModel.published).to.be.false; return admin.machineLearning().publishModel(createdModel.modelId) .then((publishedModel) => { @@ -330,16 +387,16 @@ describe('admin.machineLearning', () => { before(() => { return Promise.all([ admin.machineLearning().createModel({ - displayName: 'node-integration-list1', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list1', + tags: ['node-integ-tag-1'], }), admin.machineLearning().createModel({ - displayName: 'node-integration-list2', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list2', + tags: ['node-integ-tag-1'], }), admin.machineLearning().createModel({ - displayName: 'node-integration-list3', - tags: ['node-integration-tag-1'], + displayName: 'node-integ-list3', + tags: ['node-integ-tag-1'], })]) .then(([m1, m2, m3]: admin.machineLearning.Model[]) => { model1 = m1; @@ -357,17 +414,17 @@ describe('admin.machineLearning', () => { }); it('resolves with a list of models', () => { - return admin.machineLearning().listModels({pageSize: 100}) + return admin.machineLearning().listModels({ pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(2); expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); it('respects page size', () => { - return admin.machineLearning().listModels({pageSize: 2}) + return admin.machineLearning().listModels({ pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); expect(modelList.pageToken).not.to.be.empty; @@ -375,62 +432,63 @@ describe('admin.machineLearning', () => { }); it('filters by exact displayName', () => { - return admin.machineLearning().listModels({filter: 'displayName=node-integration-list1'}) + return admin.machineLearning().listModels({ filter: 'displayName=node-integ-list1' }) .then((modelList) => { expect(modelList.models.length).to.equal(1); expect(modelList.models[0]).to.deep.equal(model1); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); it('filters by displayName prefix', () => { - return admin.machineLearning().listModels({filter: 'displayName:node-integration-list*', pageSize: 100}) + return admin.machineLearning().listModels({ filter: 'displayName:node-integ-list*', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); expect(modelList.models).to.deep.include(model3); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); it('filters by tag', () => { - return admin.machineLearning().listModels({filter: 'tags:node-integration-tag-1', pageSize: 100}) + return admin.machineLearning().listModels({ filter: 'tags:node-integ-tag-1', pageSize: 100 }) .then((modelList) => { expect(modelList.models.length).to.be.at.least(3); expect(modelList.models).to.deep.include(model1); expect(modelList.models).to.deep.include(model2); expect(modelList.models).to.deep.include(model3); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); it('handles pageTokens properly', () => { - return admin.machineLearning().listModels({filter: 'displayName:node-integration-list*', pageSize: 2}) + return admin.machineLearning().listModels({ filter: 'displayName:node-integ-list*', pageSize: 2 }) .then((modelList) => { expect(modelList.models.length).to.equal(2); - expect(modelList.pageToken).not.to.be.empty; + expect(modelList.pageToken).not.to.be.undefined; return admin.machineLearning().listModels({ - filter: 'displayName:node-integration-list*', + filter: 'displayName:node-integ-list*', pageSize: 2, - pageToken: modelList.pageToken}) + pageToken: modelList.pageToken + }) .then((modelList2) => { expect(modelList2.models.length).to.be.at.least(1); - expect(modelList2.pageToken).to.be.empty; + expect(modelList2.pageToken).to.be.undefined; }); }); }); it('successfully returns an empty list of models', () => { - return admin.machineLearning().listModels({filter: 'displayName=non-existing-model'}) + return admin.machineLearning().listModels({ filter: 'displayName=non-existing-model' }) .then((modelList) => { expect(modelList.models.length).to.equal(0); - expect(modelList.pageToken).to.be.empty; + expect(modelList.pageToken).to.be.undefined; }); }); it('rejects with invalid argument if the filter is invalid', () => { - return admin.machineLearning().listModels({filter: 'invalidFilterItem=foo'}) + return admin.machineLearning().listModels({ filter: 'invalidFilterItem=foo' }) .should.eventually.be.rejected.and.have.property( 'code', 'machine-learning/invalid-argument'); }); @@ -464,36 +522,72 @@ describe('admin.machineLearning', () => { }); }); - function verifyModel(model: admin.machineLearning.Model, expectedOptions: admin.machineLearning.ModelOptions): void { - if (expectedOptions.displayName) { - expect(model.displayName).to.equal(expectedOptions.displayName); - } else { - expect(model.displayName).not.to.be.empty; - } - expect(model.createTime).to.not.be.empty; - expect(model.updateTime).to.not.be.empty; - expect(model.etag).to.not.be.empty; - if (expectedOptions.tags) { - expect(model.tags).to.deep.equal(expectedOptions.tags); - } else { - expect(model.tags).to.be.empty; - } - if (expectedOptions.tfliteModel) { - verifyTfliteModel(model, expectedOptions.tfliteModel.gcsTfliteUri); - } else { - expect(model.validationError).to.equal('No model file has been uploaded.'); - } - expect(model.locked).to.be.false; - } }); -function verifyTfliteModel(model: admin.machineLearning.Model, expectedGcsTfliteUri: string): void { +function verifyModel(model: admin.machineLearning.Model, expectedOptions: admin.machineLearning.ModelOptions): void { + if (expectedOptions.displayName) { + expect(model.displayName).to.equal(expectedOptions.displayName); + } else { + expect(model.displayName).not.to.be.empty; + } + expect(model.createTime).to.not.be.empty; + expect(model.updateTime).to.not.be.empty; + expect(model.etag).to.not.be.empty; + expect(model.locked).to.be.false; + if (expectedOptions.tags) { + expect(model.tags).to.deep.equal(expectedOptions.tags); + } else { + expect(model.tags).to.be.empty; + } + if ((expectedOptions as GcsTfliteModelOptions).tfliteModel?.gcsTfliteUri !== undefined) { + verifyGcsTfliteModel(model, (expectedOptions as GcsTfliteModelOptions)); + } else if ((expectedOptions as AutoMLTfliteModelOptions).tfliteModel?.automlModel !== undefined) { + verifyAutomlTfliteModel(model, (expectedOptions as AutoMLTfliteModelOptions)); + } else { + expect(model.validationError).to.equal('No model file has been uploaded.'); + } +} + +function verifyGcsTfliteModel(model: admin.machineLearning.Model, expectedOptions: GcsTfliteModelOptions): void { + const expectedGcsTfliteUri = expectedOptions.tfliteModel.gcsTfliteUri; expect(model.tfliteModel!.gcsTfliteUri).to.equal(expectedGcsTfliteUri); if (expectedGcsTfliteUri.endsWith('invalid_model.tflite')) { - expect(model.modelHash).to.be.empty; + expect(model.modelHash).to.be.undefined; expect(model.validationError).to.equal('Invalid flatbuffer format'); } else { - expect(model.modelHash).to.not.be.empty; - expect(model.validationError).to.be.empty; + expect(model.modelHash).to.not.be.undefined; + expect(model.validationError).to.be.undefined; } } + +function verifyAutomlTfliteModel(model: admin.machineLearning.Model, expectedOptions: AutoMLTfliteModelOptions): void { + const expectedAutomlReference = expectedOptions.tfliteModel.automlModel; + expect(model.tfliteModel!.automlModel).to.equal(expectedAutomlReference); + expect(model.validationError).to.be.undefined; + expect(model.tfliteModel!.sizeBytes).to.not.be.undefined; + expect(model.modelHash).to.not.be.undefined; +} + +function getAutoMLModelReference(): Promise { + let automl; + try { + const { AutoMlClient } = require('@google-cloud/automl').v1; + automl = new AutoMlClient(); + } + catch (error) { + // Returning an empty string will result in skipping the test. + return Promise.resolve(''); + } + + const parent = automl.locationPath(projectId, 'us-central1'); + return automl.listModels({ parent, filter:'displayName=admin_sdk_integ_test1' }) + .then(([models]: [any]) => { + let modelRef = ''; + for (const model of models) { + modelRef = model.name; + } + return modelRef; + }) + // Skip the test if anything goes wrong with listing the models. + .catch(() => ''); +} diff --git a/test/integration/messaging.spec.ts b/test/integration/messaging.spec.ts index 7f0d7e24f9..d8703b5335 100644 --- a/test/integration/messaging.spec.ts +++ b/test/integration/messaging.spec.ts @@ -125,7 +125,7 @@ describe('admin.messaging', () => { it('sendAll(500)', () => { const messages: admin.messaging.Message[] = []; for (let i = 0; i < 500; i++) { - messages.push({topic: `foo-bar-${i % 10}`}); + messages.push({ topic: `foo-bar-${i % 10}` }); } return admin.messaging().sendAll(messages, true) .then((response) => { @@ -172,7 +172,7 @@ describe('admin.messaging', () => { }); }); - it('sendToDeviceGroup() returns a response with success count', () => { + xit('sendToDeviceGroup() returns a response with success count', () => { return admin.messaging().sendToDeviceGroup(notificationKey, payload, options) .then((response) => { expect(typeof response.successCount).to.equal('number'); diff --git a/test/integration/remote-config.spec.ts b/test/integration/remote-config.spec.ts index 330fd0efca..f8773c1b3c 100644 --- a/test/integration/remote-config.spec.ts +++ b/test/integration/remote-config.spec.ts @@ -84,7 +84,7 @@ describe('admin.remoteConfig', () => { it('verify that the etag is read-only', () => { expect(() => { - (currentTemplate as any).etag = "new-etag"; + (currentTemplate as any).etag = 'new-etag'; }).to.throw('Cannot set property etag of # which has only a getter'); }); @@ -246,7 +246,7 @@ describe('admin.remoteConfig', () => { INVALID_JSON_STRINGS.forEach((invalidJson) => { it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { expect(() => admin.remoteConfig().createTemplateFromJSON(invalidJson)) - .to.throw(/^Failed to parse the JSON string: ([\D\w]*)\. SyntaxError: Unexpected token ([\D\w]*) in JSON at position ([0-9]*)$/); + .to.throw(/Failed to parse the JSON string/); }); }); @@ -273,7 +273,7 @@ describe('admin.remoteConfig', () => { const newTemplate = admin.remoteConfig().createTemplateFromJSON(jsonString); expect(newTemplate.etag).to.equal(sourceTemplate.etag); expect(() => { - (currentTemplate as any).etag = "new-etag"; + (currentTemplate as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter' ); diff --git a/test/integration/setup.ts b/test/integration/setup.ts index a3558ae6b2..5880a6cd0d 100644 --- a/test/integration/setup.ts +++ b/test/integration/setup.ts @@ -18,8 +18,8 @@ import * as admin from '../../lib/index'; import fs = require('fs'); import minimist = require('minimist'); import path = require('path'); -import {random} from 'lodash'; -import { Credential, GoogleOAuthAccessToken } from '../../src/auth/credential'; +import { random } from 'lodash'; +import { GoogleOAuthAccessToken } from '../../src/credential/index'; // eslint-disable-next-line @typescript-eslint/no-var-requires const chalk = require('chalk'); @@ -36,51 +36,72 @@ export let noServiceAccountApp: admin.app.App; export let cmdArgs: any; +export const isEmulator = !!process.env.FIREBASE_EMULATOR_HUB; + before(() => { - /* tslint:disable:no-console */ - let serviceAccount: any; - try { - serviceAccount = require('../resources/key.json'); - } catch (error) { - console.log(chalk.red( - 'The integration test suite requires a service account JSON file for a ' + - 'Firebase project to be saved to `test/resources/key.json`.', - error, - )); - throw error; - } + let getCredential: () => {credential?: admin.credential.Credential}; + let serviceAccountId: string; - try { - apiKey = fs.readFileSync(path.join(__dirname, '../resources/apikey.txt')).toString().trim(); - } catch (error) { - console.log(chalk.red( - 'The integration test suite requires an API key for a ' + - 'Firebase project to be saved to `test/resources/apikey.txt`.', - error, + /* tslint:disable:no-console */ + if (isEmulator) { + console.log(chalk.yellow( + 'Running integration tests against Emulator Suite. ' + + 'Some tests may be skipped due to lack of emulator support.', )); - throw error; + getCredential = () => ({}); + projectId = process.env.GCLOUD_PROJECT!; + apiKey = 'fake-api-key'; + serviceAccountId = 'fake-client-email@example.com'; + } else { + let serviceAccount: any; + try { + serviceAccount = require('../resources/key.json'); + } catch (error) { + console.log(chalk.red( + 'The integration test suite requires a service account JSON file for a ' + + 'Firebase project to be saved to `test/resources/key.json`.', + error, + )); + throw error; + } + + try { + apiKey = fs.readFileSync(path.join(__dirname, '../resources/apikey.txt')).toString().trim(); + } catch (error) { + console.log(chalk.red( + 'The integration test suite requires an API key for a ' + + 'Firebase project to be saved to `test/resources/apikey.txt`.', + error, + )); + throw error; + } + getCredential = () => ({ credential: admin.credential.cert(serviceAccount) }); + projectId = serviceAccount.project_id; + serviceAccountId = serviceAccount.client_email; } /* tslint:enable:no-console */ - projectId = serviceAccount.project_id; databaseUrl = 'https://' + projectId + '.firebaseio.com'; storageBucket = projectId + '.appspot.com'; defaultApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, storageBucket, }); nullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: null, storageBucket, }, 'null'); nonNullApp = admin.initializeApp({ - credential: admin.credential.cert(serviceAccount), + ...getCredential(), + projectId, databaseURL: databaseUrl, databaseAuthVariableOverride: { uid: generateRandomString(20), @@ -88,9 +109,14 @@ before(() => { storageBucket, }, 'nonNull'); + const noServiceAccountAppCreds = getCredential(); + if (noServiceAccountAppCreds.credential) { + noServiceAccountAppCreds.credential = new CertificatelessCredential( + noServiceAccountAppCreds.credential) + } noServiceAccountApp = admin.initializeApp({ - credential: new CertificatelessCredential(admin.credential.cert(serviceAccount)), - serviceAccountId: serviceAccount.client_email, + ...noServiceAccountAppCreds, + serviceAccountId, projectId, }, 'noServiceAccount'); @@ -106,7 +132,7 @@ after(() => { ]); }); -class CertificatelessCredential implements Credential { +class CertificatelessCredential implements admin.credential.Credential { private readonly delegate: admin.credential.Credential; constructor(delegate: admin.credential.Credential) { diff --git a/test/integration/storage.spec.ts b/test/integration/storage.spec.ts index 55e2414580..25b7a39e43 100644 --- a/test/integration/storage.spec.ts +++ b/test/integration/storage.spec.ts @@ -17,9 +17,9 @@ import * as admin from '../../lib/index'; import * as chai from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {Bucket, File} from '@google-cloud/storage'; +import { Bucket, File } from '@google-cloud/storage'; -import {projectId} from './setup'; +import { projectId } from './setup'; chai.should(); chai.use(chaiAsPromised); diff --git a/test/integration/typescript/src/example.test.ts b/test/integration/typescript/src/example.test.ts index 2b1159f696..5aa63c083d 100644 --- a/test/integration/typescript/src/example.test.ts +++ b/test/integration/typescript/src/example.test.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,9 +16,9 @@ */ import initApp from './example'; -import {expect} from 'chai'; -import {Bucket} from '@google-cloud/storage'; -import {Firestore} from '@google-cloud/firestore'; +import { expect } from 'chai'; +import { Bucket } from '@google-cloud/storage'; +import { Firestore } from '@google-cloud/firestore'; import * as admin from 'firebase-admin'; @@ -71,7 +72,8 @@ describe('Init App', () => { }); it('Should return a Cloud Storage client', () => { - const bucket: Bucket = app.storage().bucket('TestBucket'); + const storage: admin.storage.Storage = app.storage(); + const bucket: Bucket = storage.bucket('TestBucket'); expect(bucket.name).to.equal('TestBucket'); }); diff --git a/test/integration/typescript/src/example.ts b/test/integration/typescript/src/example.ts index 0b254517d1..b44dbcc163 100644 --- a/test/integration/typescript/src/example.ts +++ b/test/integration/typescript/src/example.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/test/integration/typescript/tsconfig.json b/test/integration/typescript/tsconfig.json index 5ced3e692e..6820c83387 100644 --- a/test/integration/typescript/tsconfig.json +++ b/test/integration/typescript/tsconfig.json @@ -4,7 +4,7 @@ "moduleResolution": "node", "target": "es5", "noImplicitAny": false, - "lib": ["es2015"], + "lib": ["es2018"], "outDir": "lib", "typeRoots": [ "node_modules/@types" diff --git a/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 7c73e8fe54..152364163e 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,16 +25,19 @@ import stream = require('stream'); import * as _ from 'lodash'; import * as jwt from 'jsonwebtoken'; -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseServiceInterface} from '../../src/firebase-service'; -import {FirebaseApp, FirebaseAppOptions} from '../../src/firebase-app'; -import {Credential, GoogleOAuthAccessToken, ServiceAccountCredential} from '../../src/auth/credential'; +import { AppOptions } from '../../src/firebase-namespace-api'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; +import { FirebaseApp } from '../../src/firebase-app'; +import { credential as _credential, GoogleOAuthAccessToken } from '../../src/credential/index'; +import { ServiceAccountCredential } from '../../src/credential/credential-internal'; -const ALGORITHM = 'RS256'; +const ALGORITHM = 'RS256' as const; const ONE_HOUR_IN_SECONDS = 60 * 60; export const uid = 'someUid'; export const projectId = 'project_id'; +export const projectNumber = '12345678'; +export const appId = '12345678:app:ID'; export const developerClaims = { one: 'uno', two: 'dos', @@ -51,13 +55,13 @@ export const storageBucket = 'bucketName.appspot.com'; export const credential = new ServiceAccountCredential(path.resolve(__dirname, './mock.key.json')); -export const appOptions: FirebaseAppOptions = { +export const appOptions: AppOptions = { credential, databaseURL, storageBucket, }; -export const appOptionsWithOverride: FirebaseAppOptions = { +export const appOptionsWithOverride: AppOptions = { credential, databaseAuthVariableOverride, databaseURL, @@ -65,20 +69,20 @@ export const appOptionsWithOverride: FirebaseAppOptions = { projectId, }; -export const appOptionsNoAuth: FirebaseAppOptions = { +export const appOptionsNoAuth: AppOptions = { databaseURL, }; -export const appOptionsNoDatabaseUrl: FirebaseAppOptions = { +export const appOptionsNoDatabaseUrl: AppOptions = { credential, }; -export const appOptionsAuthDB: FirebaseAppOptions = { +export const appOptionsAuthDB: AppOptions = { credential, databaseURL, }; -export class MockCredential implements Credential { +export class MockCredential implements _credential.Credential { public getAccessToken(): Promise { return Promise.resolve({ access_token: 'mock-token', // eslint-disable-line @typescript-eslint/camelcase @@ -100,7 +104,7 @@ export function mockCredentialApp(): FirebaseApp { }, appName, new FirebaseNamespace().INTERNAL); } -export function appWithOptions(options: FirebaseAppOptions): FirebaseApp { +export function appWithOptions(options: AppOptions): FirebaseApp { const namespaceInternals = new FirebaseNamespace().INTERNAL; namespaceInternals.removeApp = _.noop; return new FirebaseApp(options, appName, namespaceInternals); @@ -144,12 +148,16 @@ export const refreshToken = { type: 'refreshToken', }; +// Randomly generated JSON Web Key Sets that do not correspond to anything related to Firebase. +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const jwksResponse = require('./mock.jwks.json'); + // eslint-disable-next-line @typescript-eslint/no-var-requires export const certificateObject = require('./mock.key.json'); // Randomly generated key pairs that don't correspond to anything related to Firebase or GCP export const keyPairs = [ - /* tslint:disable:max-line-length */ + /* eslint-disable max-len */ // The private key for this key pair is identical to the one used in ./mock.key.json { public: '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAwJENcRev+eXZKvhhWLiV3Lz2MvO+naQRHo59g3vaNQnbgyduN/L4krlrJ5c6\nFiikXdtJNb/QrsAHSyJWCu8j3T9CruiwbidGAk2W0RuViTVspjHUTsIHExx9euWM0UomGvYk\noqXahdhPL/zViVSJt+Rt8bHLsMvpb8RquTIb9iKY3SMV2tCofNmyCSgVbghq/y7lKORtV/IR\nguWs6R22fbkb0r2MCYoNAbZ9dqnbRIFNZBC7itYtUoTEresRWcyFMh0zfAIJycWOJlVLDLqk\nY2SmIx8u7fuysCg1wcoSZoStuDq02nZEMw1dx8HGzE0hynpHlloRLByuIuOAfMCCYwIDAQAB\n-----END RSA PUBLIC KEY-----\n', @@ -159,12 +167,12 @@ export const keyPairs = [ public: '-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAzhI/CMRtNO45R0DD4NBXFRDYAjlB/UVGGdMJKbCIrD3Uq7r/ivedqRYUIccO\nqpeYeu9IH9iotkKq8TM0eCJAUr9WT0o5YzpGvaB8ut87xLh8SqK42VmYAvemUjI257LtDbms\nhoqzqt9Yq0sgC05b7L3r2xDTxnefeMUHYBwaerCr8PTBCu7NjK3eIWHGPouEwT46WoUpnoNm\nxdI16CoSMqtuxteG8c14qJbGR9AZujkRDntWOuL1m5KaUIc7XcAaXBt4FiPwoDoQmmCmydVC\njln3YwSrvL60iAQM6pzCxNRrJRWPYd2u7fgjir/W88w5KHOvdbUyemZWnd6SBExHuQIDAQAB\n-----END RSA PUBLIC KEY-----\n', private: '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAzhI/CMRtNO45R0DD4NBXFRDYAjlB/UVGGdMJKbCIrD3Uq7r/ivedqRYU\nIccOqpeYeu9IH9iotkKq8TM0eCJAUr9WT0o5YzpGvaB8ut87xLh8SqK42VmYAvemUjI257Lt\nDbmshoqzqt9Yq0sgC05b7L3r2xDTxnefeMUHYBwaerCr8PTBCu7NjK3eIWHGPouEwT46WoUp\nnoNmxdI16CoSMqtuxteG8c14qJbGR9AZujkRDntWOuL1m5KaUIc7XcAaXBt4FiPwoDoQmmCm\nydVCjln3YwSrvL60iAQM6pzCxNRrJRWPYd2u7fgjir/W88w5KHOvdbUyemZWnd6SBExHuQID\nAQABAoIBAQDJ9iv9BbYaGBfe82SGIuoV5Uou87ru5EPN73yddTydwoN6Q21L316PZuoYKKUB\nIE36viSrwYWoCzLJ7etQihEMiCWo1A/mZikKlA1qgHptVHnMFCqiKiLHVbuV90zETCH0P7MM\nsUdhAkA+sQQY0JVbMs/DBXzomDic/k06LpDtCBNdjL7UIT5KyFbBqit+cV6H91Ujqg8MmzrU\ntOSw+63oSqZJkT6WPuA/NJNXqtFF+0aOKNX1ttrrTzSDhyp6AxOO7Wm++dpYBtcfnOc3EG65\nul9PfKsJwVZFVO+AAZwdLCeKjtCtWeJc/yXvSj2NTsjs3FKJkRAmmiMp5tH+vbE5AoGBAOhn\nKTXGI+ofA3iggByt2InCU+YIXsw1EbbhH4LGB8yyUA2SIjZybwUMKCkoMxmEumFP/FWgOL2w\nLlClqf9vZg9dBy8bDINJHm+9roYRO0/EhHA6IDSC+0X5BPZOexrBI07HJI7w7Y0WHFU8jK53\n55ps2YGT20n7haRMbbPMrq/3AoGBAOL+pY8bgCnKmeG2inun4FuD+0/aXAySXi70/BAABeHH\npogEfc0jv5SgygTiuC/2T84Jmsg0Y6M2l86srMrMA07xtyMbfRq7zih+K+EDoQ9HAwhDqxX5\nM7E8fPXscDzH2Y361QiGAQpjUcMix3hDV8oK537rYOmCYku18ZsVkjnPAoGAbE1u4fVlVTyA\ntJ0vNq45Q/GAgamS690rVStSMPIyPk02iyx3ryHi5NpGeO+X6KN269SHhiu1ZYiN/N1G/Jeg\nWzaCG4yiZygS/AXMKAQtvL2a7mXYDkCf8nrauiHWsqAg4RxiyA401dPg/kPKV5/fGZLyRbVu\nsup43BkV4n1XRv8CgYAmUIE1dJjfdPkgZiVd1epCyDZFNkBPRu1q06MwODDF+WMcllV9qMkP\nl0xCItqgDd1Ok8RygpVG2VIqam8IFAOC8b3NyTgGqSiVISba5jfrUjsqy/E21kdpZSJaiDwx\npjIMiwgmVigazsTgQSCWJhfNXKXSgHxtLbrVuLI9URjLdQKBgQDProyaG7pspt6uUdqMTa4+\nGVkUg+gIt5aVTf/Lb25K3SHA1baPamtbTDDf6vUjeJtTG+O+RMGqK5mB2MywjVHJdMGcJ44e\nogIh9eWY450oUoVBjEsdUd7Ef5KcpMFDUVFJwzCY371+Loqh2KYAk8WUSRzwGuw2QtLPO/L/\nQkKj4Q==\n-----END RSA PRIVATE KEY-----\n', }, - /* tslint:enable:max-line-length */ + /* eslint-enable max-len */ ]; // Randomly generated an X.509 certs using https://www.samltool.com/self_signed_certs.php export const x509CertPairs = [ - /* tslint:disable:max-line-length */ + /* eslint-disable max-len */ { public: '-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czEL\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGUwHhcNMTgxMjA2MDc1MTUxWhcNMjgxMjAzMDc1MTUx\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAKphmggjiVgqMLXyzvI7cKphscIIQ+wcv7Dld6MD4aKv\n7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDotfilaG6NxpF/GfVVmMalzwWp0mT8+H\nyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lqC21UhA3WVgjPOLqB42rlE9gPnZLB\nAgMBAAGjUDBOMB0GA1UdDgQWBBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAfBgNVHSME\nGDAWgBS0iM7WnbCNOnieOP1HIA+Oz/ML+zAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAF3jBgS+wP+K/jTupEQur6iaqS4UvXd//d4vo1MV06oTLQMTz+rP\nOSMDNwxzfaOn6vgYLKP/Dcy9dSTnSzgxLAxfKvDQZA0vE3udsw0Bd245MmX4+GOp\nlbrN99XP1u+lFxCSdMUzvQ/jW4ysw/Nq4JdJ0gPAyPvL6Qi/3mQdIQwx\n-----END CERTIFICATE-----\n', private: '-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKphmggjiVgqMLXy\nzvI7cKphscIIQ+wcv7Dld6MD4aKv7Jqr8ltujMxBUeY4LFEKw8Terb01snYpDotf\nilaG6NxpF/GfVVmMalzwWp0mT8+HyzyPj89mRcozu17RwuooR6n1ofXjGcBE86lq\nC21UhA3WVgjPOLqB42rlE9gPnZLBAgMBAAECgYAwZ7g2FbqAZMQf/RKUORTiIw04\nXdbGLsi6/gZGNuUUrjxfGPiqxzaTFP+qk0zr3U4PEWB0v9uqvDFYoVURDhT7isxm\nH5bc6dxwvBRIy8tLtvxo0jMTotJaBhEHP3YMKxbC7lxo3PV5HLIve5nf9ChOypKp\n4zbP4d1IJjpu8ggrbQJBANoPCrJyXjsDgh8WAEpALAsgM4ugyJwdk8AHJUTy3IeJ\niYYB/RLVpYW8LI1dmqN5NPKbyKE+dsdSiiEpclsocl8CQQDIBt5DbO+tEGr5BGsk\nBi+P3E1M3KVV2eJv+inlgYkYeS/cdd5CJczCDwxeDk8DXsKvmOp0LCHeU2sCKjSy\nF07fAkB86KLjB1ptCZxu/CZcYhgYo3CDai2gJ90r4av6q/eheCqb5eW29UUkr18B\n932OaO7ojk5F90cI9IIFbv1/tFKXAkEAnrXUZWtqQMdmGW+IE21VD7CdJP9tsFDR\nekfkNlYxkVmWwDZFw/Z6IQAPsBFqYCIwF2Qdo0/hD6bgoTcb2LLlwQJATqOMr7yr\neYKLJ+edhwMHx4U5ZIT8l/MjDv4/6L6FgGYVo7gNjjIIsDXUOo3PlBOWe6fxb5+f\ntFlwxZNz+g9ONg==\n-----END PRIVATE KEY-----\n', @@ -173,16 +181,25 @@ export const x509CertPairs = [ public: '-----BEGIN CERTIFICATE-----\nMIICZjCCAc+gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBQMQswCQYDVQQGEwJ1czEL\nMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAPBgNVBAMMCGFjbWUuY29tMRIw\nEAYDVQQHDAlTdW5ueXZhbGUwHhcNMTgxMjA2MDc1ODE4WhcNMjgxMjAzMDc1ODE4\nWjBQMQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDTALBgNVBAoMBEFjbWUxETAP\nBgNVBAMMCGFjbWUuY29tMRIwEAYDVQQHDAlTdW5ueXZhbGUwgZ8wDQYJKoZIhvcN\nAQEBBQADgY0AMIGJAoGBAKuzYKfDZGA6DJgQru3wNUqv+S0hMZfP/jbp8ou/8UKu\nrNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWwA53BxeP/oyXmJ93uHG5mFRAsVAUKs\ncVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6IBoZQzNNK4n1AATqwo+z7a0pfRrJ\nAgMBAAGjUDBOMB0GA1UdDgQWBBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAfBgNVHSME\nGDAWgBSKmi/ZKMuLN0ES7/jPa7q7jAjPiDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3\nDQEBDQUAA4GBAAg2a2kSn05NiUOuWOHwPUjW3wQRsGxPXtbhWMhmNdCfKKteM2+/\nLd/jz5F3qkOgGQ3UDgr3SHEoWhnLaJMF4a2tm6vL2rEIfPEK81KhTTRxSsAgMVbU\nJXBz1md6Ur0HlgQC7d1CHC8/xi2DDwHopLyxhogaZUxy9IaRxUEa2vJW\n-----END CERTIFICATE-----\n', private: '-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKuzYKfDZGA6DJgQ\nru3wNUqv+S0hMZfP/jbp8ou/8UKurNeX7cfCgt3yxoGCJYKmF6t5mvo76JY0MWwA\n53BxeP/oyXmJ93uHG5mFRAsVAUKscVVb0Xi6ujxZGVdDWFV696L0BNOoHTfXmac6\nIBoZQzNNK4n1AATqwo+z7a0pfRrJAgMBAAECgYBG15vpnBSuH0VS+I80XQef6TtG\nA4wStx6MSbppLqi8epWV3nmdEgQszx5YEPqpDR53AZWP6WftkVtS1IypOChTwRIh\n73vheFJ4XYqjoU+2OUtj7hhMMHDBFhw7W3Jvz4PkPu9drmzBS8N5Dd38ROwhwoS3\nUD/18pxXXyd61s/+gQJBANSuA7fRna1qXmRmdwpQR1Mebh0dw2ZgOn4ekIgsfmgP\nGPznhsjWQEuT1BxIS8R8x4ZmCJY4W89GfUBLtWprBTsCQQDOrIyHCOzOmNYXcgRT\nhW+ZiSi+46FAYqCKawIwlq2M0GsJaMTdXFQFKTmnxiNvWxxDOeZcIsrc5uwEwr6A\n3I/LAkEAwuFBHurAZOsW20DYy2aMNKmplJx1NBXxAyfWoDDFE2ziJLuyUc2g1J/8\nuH22j7EW0xwjuiKiXeflVUkKTx0JiQJAUQb5OV/YZ88n8J008QHZlRpfLSfVaobA\nZkQ54Y7Rj+mObWvz8s1l63gUMKDP97KCzCCBHhJN8nlegydOxPq0LQJADBjkunGt\nfIGv6A3SG5/5nRYI1gHQsq30BaAPwx6BuDBtnaf5BpzcFvu1JMNHoVFYzmiykpwX\n1zUhaAtcX2BV9g==\n-----END PRIVATE KEY-----\n', }, - /* tslint:enable:max-line-length */ + /* eslint-enable max-len */ ]; +// 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. * @return {string} A mocked Firebase ID token with any provided overrides included. */ -export function generateIdToken(overrides?: object): string { +export function generateIdToken(overrides?: object, claims?: object): string { const options = _.assign({ audience: projectId, expiresIn: ONE_HOUR_IN_SECONDS, @@ -194,7 +211,12 @@ export function generateIdToken(overrides?: object): string { }, }, overrides); - return jwt.sign(developerClaims, certificateObject.private_key, options); + const payload = { + ...developerClaims, + ...claims, + }; + + return jwt.sign(payload, certificateObject.private_key, options); } /** @@ -219,15 +241,25 @@ export function generateSessionCookie(overrides?: object, expiresIn?: number): s return jwt.sign(developerClaims, certificateObject.private_key, options); } -export function firebaseServiceFactory( - firebaseApp: FirebaseApp, - extendApp?: (props: object) => void, // eslint-disable-line @typescript-eslint/no-unused-vars -): FirebaseServiceInterface { - const result = { - app: firebaseApp, - INTERNAL: {}, - }; - return result as FirebaseServiceInterface; +/** + * 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. */ diff --git a/test/unit/app-check/app-check-api-client-internal.spec.ts b/test/unit/app-check/app-check-api-client-internal.spec.ts new file mode 100644 index 0000000000..fba1fba20d --- /dev/null +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -0,0 +1,238 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import { HttpClient } from '../../../src/utils/api-request'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { getSdkVersion } from '../../../src/utils'; + +import { FirebaseApp } from '../../../src/firebase-app'; +import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +const expect = chai.expect; + +describe('AppCheckApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + const EXPECTED_HEADERS = { + 'Authorization': 'Bearer mock-token', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + }; + + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + const APP_ID = '1:1234:android:1234'; + + const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; + + const TEST_RESPONSE = { + attestationToken: 'token', + ttl: '3s' + }; + + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + const clientWithoutProjectId = new AppCheckApiClient( + mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: AppCheckApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new AppCheckApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new AppCheckApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to admin.appCheck() must be a valid Firebase app instance.'); + }); + }); + + describe('exchangeToken', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should throw given no appId', () => { + expect(() => { + (apiClient as any).exchangeToken(TEST_TOKEN_TO_EXCHANGE); + }).to.throw('appId` must be a non-empty string.'); + }); + + const invalidAppIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given a non-string appId: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, invalidAppId as any); + }).to.throw('appId` must be a non-empty string.'); + }); + }); + + it('should throw given an empty string appId', () => { + expect(() => { + apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, ''); + }).to.throw('appId` must be a non-empty string.'); + }); + + it('should throw given no customToken', () => { + expect(() => { + (apiClient as any).exchangeToken(undefined, APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + + const invalidCustomTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidCustomTokens.forEach((invalidCustomToken) => { + it('should throw given a non-string customToken: ' + JSON.stringify(invalidCustomToken), () => { + expect(() => { + apiClient.exchangeToken(invalidCustomToken as any, APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + }); + + it('should throw given an empty string customToken', () => { + expect(() => { + apiClient.exchangeToken('', APP_ID); + }).to.throw('customToken` must be a non-empty string.'); + }); + + it('should reject when a full platform error response is received', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + stubs.push(stub); + const expected = new FirebaseAppCheckError('not-found', 'Requested entity not found'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + const expected = new FirebaseAppCheckError('unknown-error', 'Unknown server error: {}'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + stubs.push(stub); + const expected = new FirebaseAppCheckError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseAppError', () => { + const expected = new FirebaseAppError('network-error', 'socket hang up'); + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(expected); + stubs.push(stub); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + ['', 'abc', '3s2', 'sssa', '3.000000001', '3.2', null, NaN, true, [], {}, 100, 1.2, -200, -2.4] + .forEach((invalidDuration) => { + it(`should throw if the returned ttl duration is: ${invalidDuration}`, () => { + const response = deepCopy(TEST_RESPONSE); + (response as any).ttl = invalidDuration; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + const expected = new FirebaseAppCheckError( + 'invalid-argument', '`ttl` must be a valid duration string with the suffix `s`.'); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .should.eventually.be.rejected.and.deep.include(expected); + }); + }); + + it('should resolve with the App Check token on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200)); + stubs.push(stub); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .then((resp) => { + expect(resp.token).to.deep.equal(TEST_RESPONSE.attestationToken); + expect(resp.ttlMillis).to.deep.equal(3000); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: `https://firebaseappcheck.googleapis.com/v1beta/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, + headers: EXPECTED_HEADERS, + data: { customToken: TEST_TOKEN_TO_EXCHANGE } + }); + }); + }); + + new Map([['3s', 3000], ['4.1s', 4100], ['3.000000001s', 3000], ['3.000001s', 3000]]) + .forEach((ttlMillis, ttlString) => { // value, key, map + // 3 seconds with 0 nanoseconds expressed as "3s" + // 3 seconds and 1 nanosecond expressed as "3.000000001s" + // 3 seconds and 1 microsecond expressed as "3.000001s" + it(`should resolve with ttlMillis as ${ttlMillis} when ttl + from server is: ${ttlString}`, () => { + const response = deepCopy(TEST_RESPONSE); + (response as any).ttl = ttlString; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(response, 200)); + stubs.push(stub); + return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) + .then((resp) => { + expect(resp.token).to.deep.equal(response.attestationToken); + expect(resp.ttlMillis).to.deep.equal(ttlMillis); + }); + }); + }); + }); +}); diff --git a/test/unit/app-check/app-check.spec.ts b/test/unit/app-check/app-check.spec.ts new file mode 100644 index 0000000000..05c598f301 --- /dev/null +++ b/test/unit/app-check/app-check.spec.ts @@ -0,0 +1,204 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; + +import { FirebaseApp } from '../../../src/firebase-app'; +import { AppCheck } from '../../../src/app-check/app-check'; +import { AppCheckApiClient, FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import { AppCheckTokenGenerator } from '../../../src/app-check/token-generator'; +import { HttpClient } from '../../../src/utils/api-request'; +import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; +import { AppCheckTokenVerifier } from '../../../src/app-check/token-verifier'; + +const expect = chai.expect; + +describe('AppCheck', () => { + + const INTERNAL_ERROR = new FirebaseAppCheckError('internal-error', 'message'); + const APP_ID = '1:1234:android:1234'; + const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; + + let appCheck: AppCheck; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + appCheck = new AppCheck(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidApps.forEach((invalidApp) => { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const appCheckAny: any = AppCheck; + return new appCheckAny(invalidApp); + }).to.throw( + 'First argument passed to admin.appCheck() must be a valid Firebase app ' + + 'instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const appCheckAny: any = AppCheck; + return new appCheckAny(); + }).to.throw( + 'First argument passed to admin.appCheck() must be a valid Firebase app ' + + 'instance.'); + }); + + it('should reject when initialized without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + const appCheckWithoutProjectId = new AppCheck(mockCredentialApp); + const stub = sinon.stub(AppCheckTokenGenerator.prototype, 'createCustomToken') + .resolves(TEST_TOKEN_TO_EXCHANGE); + stubs.push(stub); + return appCheckWithoutProjectId.createToken(APP_ID) + .should.eventually.rejectedWith(noProjectId); + }); + + it('should reject when failed to contact the Metadata server', () => { + // Remove the Project ID to force a request to the Metadata server + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const appCheckWithoutProjectId = new AppCheck(mockCredentialApp); + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(new Error('network error.')); + stubs.push(stub); + const expected = 'Failed to determine service account. Make sure to initialize the SDK ' + + 'with a service account credential. Alternatively specify a service account with ' + + 'iam.serviceAccounts.signBlob permission. Original error: ' + + 'Error: network error.'; + return appCheckWithoutProjectId.createToken(APP_ID) + .should.eventually.be.rejectedWith(expected); + }); + + it('should reject when failed to sign the token', () => { + const expected = 'sign error'; + const stub = sinon.stub(ServiceAccountSigner.prototype, 'sign') + .rejects(new Error(expected)); + stubs.push(stub); + return appCheck.createToken(APP_ID) + .should.eventually.be.rejectedWith(expected); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new AppCheck(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(appCheck.app).to.equal(mockApp); + }); + }); + + describe('createToken', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(AppCheckApiClient.prototype, 'exchangeToken') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return appCheck.createToken(APP_ID) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should 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', + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: 'app-id', + aud: ['123456', 'project-id'], + exp: 1617741496, + iat: 1516239022, + }; + const stub = sinon + .stub(AppCheckTokenVerifier.prototype, 'verifyToken') + .resolves(response); + stubs.push(stub); + return appCheck.verifyToken('token') + .then((tokenResponse) => { + expect(tokenResponse.appId).equals('app-id'); + expect(tokenResponse.token).equals(response); + }); + }); + }); +}); diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts new file mode 100644 index 0000000000..2c612c1b64 --- /dev/null +++ b/test/unit/app-check/token-generator.spec.ts @@ -0,0 +1,359 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as jwt from 'jsonwebtoken'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as mocks from '../../resources/mocks'; + +import { + appCheckErrorFromCryptoSignerError, + AppCheckTokenGenerator +} from '../../../src/app-check/token-generator'; +import { + CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner +} from '../../../src/utils/crypto-signer'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; +import { FirebaseAppCheckError } from '../../../src/app-check/app-check-api-client-internal'; +import * as utils from '../utils'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +const ALGORITHM = 'RS256'; +const FIVE_MIN_IN_SECONDS = 60 * 5; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; + +/** + * Verifies a token is signed with the private key corresponding to the provided public key. + * + * @param {string} token The token to verify. + * @param {string} publicKey The public key to use to verify the token. + * @return {Promise} A promise fulfilled with the decoded token if it is valid; otherwise, a rejected promise. + */ +function verifyToken(token: string, publicKey: string): Promise { + return new Promise((resolve, reject) => { + jwt.verify(token, publicKey, { + algorithms: [ALGORITHM], + }, (err, res) => { + if (err) { + reject(err); + } else { + resolve(res as object); + } + }); + }); +} + +describe('AppCheckTokenGenerator', () => { + const cert = new ServiceAccountCredential(mocks.certificateObject); + const APP_ID = 'test-app-id'; + + let clock: sinon.SinonFakeTimers | undefined; + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + describe('Constructor', () => { + it('should throw given no arguments', () => { + expect(() => { + // Need to overcome the type system to allow a call with no parameter + const anyFirebaseAppCheckTokenGenerator: any = AppCheckTokenGenerator; + return new anyFirebaseAppCheckTokenGenerator(); + }).to.throw('Must provide a CryptoSigner to use AppCheckTokenGenerator'); + }); + }); + + const invalidSigners: any[] = [null, NaN, 0, 1, true, false, '', 'a', [], _.noop]; + invalidSigners.forEach((invalidSigner) => { + it('should throw given invalid signer: ' + JSON.stringify(invalidSigner), () => { + expect(() => { + return new AppCheckTokenGenerator(invalidSigner as any); + }).to.throw('Must provide a CryptoSigner to use AppCheckTokenGenerator'); + }); + }); + + describe('createCustomToken()', () => { + const tokenGenerator = new AppCheckTokenGenerator(new ServiceAccountSigner(cert)); + + it('should throw given no appId', () => { + expect(() => { + (tokenGenerator as any).createCustomToken(); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + + const invalidAppIds = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidAppIds.forEach((invalidAppId) => { + it('should throw given a non-string appId: ' + JSON.stringify(invalidAppId), () => { + expect(() => { + tokenGenerator.createCustomToken(invalidAppId as any); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + }); + + it('should throw given an empty string appId', () => { + expect(() => { + tokenGenerator.createCustomToken(''); + }).to.throw(FirebaseAppCheckError).with.property('code', 'app-check/invalid-argument'); + }); + + 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 } = { + // eslint-disable-next-line @typescript-eslint/camelcase + 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 } = { + // eslint-disable-next-line @typescript-eslint/camelcase + 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 } = { + // eslint-disable-next-line @typescript-eslint/camelcase + 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..27d0be2fbf --- /dev/null +++ b/test/unit/app-check/token-verifier.spec.ts @@ -0,0 +1,245 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; +import * as nock from 'nock'; + +import { AppCheckTokenVerifier } from '../../../src/app-check/token-verifier'; +import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; + +const expect = chai.expect; + +const ONE_HOUR_IN_SECONDS = 60 * 60; + +describe('AppCheckTokenVerifier', () => { + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let tokenVerifier: AppCheckTokenVerifier; + let clock: sinon.SinonFakeTimers | undefined; + + before(() => { + tokenVerifier = new AppCheckTokenVerifier(mocks.app()); + }); + + after(() => { + nock.cleanAll(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + describe('verifyJWT()', () => { + let mockedRequests: nock.Scope[] = []; + let stubs: sinon.SinonStub[] = []; + + afterEach(() => { + _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); + mockedRequests = []; + + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should throw given no App Check token', () => { + expect(() => { + (tokenVerifier as any).verifyToken(); + }).to.throw('App check token must be a non-null string'); + }); + + const invalidTokens = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop]; + invalidTokens.forEach((invalidToken) => { + it('should throw given a non-string App Check token: ' + JSON.stringify(invalidToken), () => { + expect(() => { + tokenVerifier.verifyToken(invalidToken as any); + }).to.throw('App check token must be a non-null string'); + }); + }); + + it('should throw given an empty string App Check token', () => { + return tokenVerifier.verifyToken('') + .should.eventually.be.rejectedWith('Decoding App Check token failed'); + }); + + it('should be rejected given an invalid App Check token', () => { + return tokenVerifier.verifyToken('invalid-token') + .should.eventually.be.rejectedWith('Decoding App Check token failed'); + }); + + it('should throw if the token verifier was initialized with no "project_id"', () => { + const tokenVerifierWithNoProjectId = new AppCheckTokenVerifier(mocks.mockCredentialApp()); + const expected = 'Must initialize app with a cert credential or set your Firebase project ID as ' + + 'the GOOGLE_CLOUD_PROJECT environment variable to verify an App Check token.'; + return tokenVerifierWithNoProjectId.verifyToken('app.check.token') + .should.eventually.be.rejectedWith(expected); + }); + + it('should be rejected given an App Check token with an incorrect algorithm', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + algorithm: 'HS256', + }); + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect algorithm'); + }); + + const invalidAudiences = [ + 'incorrectAudience', [], [mocks.projectNumber, mocks.projectId], + ['projects/' + mocks.projectNumber, mocks.projectId] + ]; + invalidAudiences.forEach((invalidAudience) => { + it('should be rejected given an App Check token with an incorrect audience:' + + JSON.stringify(invalidAudience), () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + audience: invalidAudience, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect "aud" (audience) claim'); + }); + }); + + it('should be rejected given an App Check token with an incorrect issuer', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + issuer: 'incorrectIssuer', + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has incorrect "iss" (issuer) claim'); + }); + + it('should be rejected given an App Check token with an empty subject', () => { + const mockAppCheckToken = mocks.generateAppCheckToken({ + subject: '', + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has an empty string "sub" (subject) claim'); + }); + + it('should be rejected when the verifier throws no maching kid error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.NO_MATCHING_KID, 'No matching key ID.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken({ + header: { + kid: 'wrongkid', + }, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + + it('should be rejected when the verifier throws expired token error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has expired. ' + + 'Get a fresh App Check token from your client app and try again.') + .and.have.property('code', 'app-check/app-check-token-expired'); + }); + + it('should be rejected when the verifier throws invalid signature error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('The provided App Check token has invalid signature'); + }); + + it('should be rejected when the verifier throws key fetch error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.KEY_FETCH_ERROR, 'Error fetching Json Web Keys.')); + stubs.push(verifierStub); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.rejectedWith('Error fetching Json Web Keys.'); + }); + + it('should be fulfilled when the kid is not present in the header (should try all the keys)', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAppCheckToken = mocks.generateAppCheckToken({ + header: {}, + }); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: ['projects/' + mocks.projectNumber, 'projects/' + mocks.projectId], + iss: 'https://firebaseappcheck.googleapis.com/' + mocks.projectNumber, + sub: mocks.appId, + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: mocks.appId, + }); + }); + + it('should be fulfilled with decoded claims given a valid App Check token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); + + clock = sinon.useFakeTimers(1000); + + const mockAppCheckToken = mocks.generateAppCheckToken(); + + return tokenVerifier.verifyToken(mockAppCheckToken) + .should.eventually.be.fulfilled.and.deep.equal({ + one: 'uno', + two: 'dos', + iat: 1, + exp: ONE_HOUR_IN_SECONDS + 1, + aud: ['projects/' + mocks.projectNumber, 'projects/' + mocks.projectId], + iss: 'https://firebaseappcheck.googleapis.com/' + mocks.projectNumber, + sub: mocks.appId, + // eslint-disable-next-line @typescript-eslint/camelcase + app_id: mocks.appId, + }); + }); + + }); +}); diff --git a/test/unit/auth/action-code-settings-builder.spec.ts b/test/unit/auth/action-code-settings-builder.spec.ts index 78eace7a04..ceadde3e9b 100644 --- a/test/unit/auth/action-code-settings-builder.spec.ts +++ b/test/unit/auth/action-code-settings-builder.spec.ts @@ -19,8 +19,8 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {ActionCodeSettingsBuilder} from '../../../src/auth/action-code-settings-builder'; -import {AuthClientErrorCode} from '../../../src/utils/error'; +import { ActionCodeSettingsBuilder } from '../../../src/auth/action-code-settings-builder'; +import { AuthClientErrorCode } from '../../../src/utils/error'; chai.should(); @@ -139,7 +139,7 @@ describe('ActionCodeSettingsBuilder', () => { return new ActionCodeSettingsBuilder({ url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, - iOS: {bundleId}, + iOS: { bundleId }, } as any); }).to.throw('"ActionCodeSettings.iOS.bundleId" must be a valid non-empty string.'); }); @@ -175,7 +175,7 @@ describe('ActionCodeSettingsBuilder', () => { return new ActionCodeSettingsBuilder({ url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, - android: {packageName}, + android: { packageName }, } as any); }).to.throw('"ActionCodeSettings.android.packageName" must be a valid non-empty string.'); }); diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts old mode 100755 new mode 100644 index 15f90407d7..29ccf6eb4d --- 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"); @@ -25,9 +26,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {deepCopy, deepExtend} from '../../../src/utils/deep-copy'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {HttpClient, HttpRequestConfig} from '../../../src/utils/api-request'; +import { deepCopy, deepExtend } from '../../../src/utils/deep-copy'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; import * as validator from '../../../src/utils/validator'; import { AuthRequestHandler, FIREBASE_AUTH_GET_ACCOUNT_INFO, FIREBASE_AUTH_GET_ACCOUNTS_INFO, @@ -36,16 +37,24 @@ import { RESERVED_CLAIMS, FIREBASE_AUTH_UPLOAD_ACCOUNT, FIREBASE_AUTH_CREATE_SESSION_COOKIE, EMAIL_ACTION_REQUEST_TYPES, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import {UserImportBuilder, UserImportRecord} from '../../../src/auth/user-import-builder'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; -import {ActionCodeSettingsBuilder} from '../../../src/auth/action-code-settings-builder'; -import { - OIDCAuthProviderConfig, SAMLAuthProviderConfig, OIDCUpdateAuthProviderRequest, - SAMLUpdateAuthProviderRequest, SAMLConfigServerResponse, -} from '../../../src/auth/auth-config'; -import {UserIdentifier} from '../../../src/auth/identifier'; -import {TenantOptions} from '../../../src/auth/tenant'; -import { UpdateRequest, UpdateMultiFactorInfoRequest } from '../../../src/auth/user-record'; +import { 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 { auth } from '../../../src/auth/index'; + +import UserImportRecord = auth.UserImportRecord; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import UserIdentifier = auth.UserIdentifier; +import UpdateRequest = auth.UpdateRequest; +import UpdateMultiFactorInfoRequest = auth.UpdateMultiFactorInfoRequest; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; chai.should(); chai.use(sinonChai); @@ -109,7 +118,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getRequestValidator(); it('should succeed with valid parameters passed', () => { - const validRequest = {idToken: 'ID_TOKEN', validDuration: 60 * 60}; + const validRequest = { idToken: 'ID_TOKEN', validDuration: 60 * 60 }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -118,7 +127,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { }); it('should succeed with duration set at minimum allowed', () => { const validDuration = 60 * 5; - const validRequest = {idToken: 'ID_TOKEN', validDuration}; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -127,7 +136,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { }); it('should succeed with duration set at maximum allowed', () => { const validDuration = 60 * 60 * 24 * 14; - const validRequest = {idToken: 'ID_TOKEN', validDuration}; + const validRequest = { idToken: 'ID_TOKEN', validDuration }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -135,14 +144,14 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { expect(isNumber).to.have.been.calledOnce.and.calledWith(validDuration); }); it('should fail when idToken not passed', () => { - const invalidRequest = {validDuration: 60 * 60}; + const invalidRequest = { validDuration: 60 * 60 }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(undefined); }); it('should fail when validDuration not passed', () => { - const invalidRequest = {idToken: 'ID_TOKEN'}; + const invalidRequest = { idToken: 'ID_TOKEN' }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -151,13 +160,13 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('called with invalid parameters', () => { it('should fail with invalid idToken', () => { expect(() => { - return requestValidator({idToken: '', validDuration: 60 * 60}); + return requestValidator({ idToken: '', validDuration: 60 * 60 }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with invalid validDuration', () => { expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: 'invalid'}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: 'invalid' }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith('invalid'); @@ -166,7 +175,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { // Duration less 5 minutes. const outOfBoundDuration = 60 * 5 - 1; expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: outOfBoundDuration}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: outOfBoundDuration }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith(outOfBoundDuration); @@ -175,7 +184,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { // Duration greater than 14 days. const outOfBoundDuration = 60 * 60 * 24 * 14 + 1; expect(() => { - return requestValidator({idToken: 'ID_TOKEN', validDuration: outOfBoundDuration}); + return requestValidator({ idToken: 'ID_TOKEN', validDuration: outOfBoundDuration }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith('ID_TOKEN'); expect(isNumber).to.have.been.calledOnce.and.calledWith(outOfBoundDuration); @@ -185,7 +194,7 @@ describe('FIREBASE_AUTH_CREATE_SESSION_COOKIE', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_CREATE_SESSION_COOKIE.getResponseValidator(); it('should succeed with sessionCookie returned', () => { - const validResponse = {sessionCookie: 'SESSION_COOKIE'}; + const validResponse = { sessionCookie: 'SESSION_COOKIE' }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -257,7 +266,7 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_DOWNLOAD_ACCOUNT.getRequestValidator(); it('should succeed with valid maxResults passed', () => { - const validRequest = {maxResults: 500}; + const validRequest = { maxResults: 500 }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -286,31 +295,31 @@ describe('FIREBASE_AUTH_DOWNLOAD_ACCOUNT', () => { describe('called with invalid parameters', () => { it('should fail with invalid maxResults', () => { expect(() => { - return requestValidator({maxResults: ''}); + return requestValidator({ maxResults: '' }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with zero maxResults', () => { expect(() => { - return requestValidator({maxResults: 0}); + return requestValidator({ maxResults: 0 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(0); }); it('should fail with negative maxResults', () => { expect(() => { - return requestValidator({maxResults: -500}); + return requestValidator({ maxResults: -500 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(-500); }); it('should fail with maxResults exceeding allowed limit', () => { expect(() => { - return requestValidator({maxResults: 1001}); + return requestValidator({ maxResults: 1001 }); }).to.throw(); expect(isNumber).to.have.been.calledOnce.and.calledWith(1001); }); it('should fail with invalid nextPageToken', () => { expect(() => { - return requestValidator({maxResults: 1000, nextPageToken: ['PAGE_TOKEN']}); + return requestValidator({ maxResults: 1000, nextPageToken: ['PAGE_TOKEN'] }); }).to.throw(); expect(isNonEmptyString).to.have.been.calledOnce.and.calledWith(['PAGE_TOKEN']); }); @@ -328,31 +337,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'}]}; + 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(); @@ -361,17 +376,23 @@ describe('FIREBASE_AUTH_GET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = {users: []}; + const validResponse: object = { users: [{ localId: 'foo' }] }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); }); - it('should fail when users is not returned', () => { + it('should fail when the response object is empty', () => { const invalidResponse = {}; expect(() => { responseValidator(invalidResponse); }).to.throw(); }); + it('should fail when the response object has an empty list of users', () => { + const invalidResponse = { users: [] }; + expect(() => { + responseValidator(invalidResponse); + }).to.throw(); + }); }); }); @@ -385,31 +406,31 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getRequestValidator(); it('should succeed with localId passed', () => { - const validRequest = {localId: ['1234']}; + const validRequest = { localId: ['1234'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with email passed', () => { - const validRequest = {email: ['user@example.com']}; + const validRequest = { email: ['user@example.com'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with phoneNumber passed', () => { - const validRequest = {phoneNumber: ['+11234567890']}; + const validRequest = { phoneNumber: ['+11234567890'] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should succeed with federatedUserId passed', () => { - const validRequest = {federatedUserId: [{providerId: 'google.com', rawId: 'google_uid'}]}; + const validRequest = { federatedUserId: [{ providerId: 'google.com', rawId: 'google_uid' }] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should fail when neither localId, email or phoneNumber are passed', () => { - const invalidRequest = {bla: ['1234']}; + const invalidRequest = { bla: ['1234'] }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -420,9 +441,9 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { email: ['user1@example.com', 'user2@example.com'], phoneNumber: ['+15555550001', '+15555550002'], federatedUserId: [ - {providerId: 'google.com', rawId: 'google_uid1'}, - {providerId: 'google.com', rawId: 'google_uid2'} - ]}; + { providerId: 'google.com', rawId: 'google_uid1' }, + { providerId: 'google.com', rawId: 'google_uid2' } + ] }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -431,7 +452,7 @@ describe('FIREBASE_AUTH_GET_ACCOUNTS_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_GET_ACCOUNTS_INFO.getResponseValidator(); it('should succeed with users returned', () => { - const validResponse: object = {users: []}; + const validResponse: object = { users: [] }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -463,13 +484,13 @@ describe('FIREBASE_AUTH_DELETE_ACCOUNT', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_DELETE_ACCOUNT.getRequestValidator(); it('should succeed with localId passed', () => { - const validRequest = {localId: '1234'}; + const validRequest = { localId: '1234' }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); }); it('should fail when localId not passed', () => { - const invalidRequest = {bla: '1234'}; + const invalidRequest = { bla: '1234' }; expect(() => { return requestValidator(invalidRequest); }).to.throw(); @@ -512,7 +533,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('requestValidator', () => { const requestValidator = FIREBASE_AUTH_SET_ACCOUNT_INFO.getRequestValidator(); it('should succeed with valid localId passed', () => { - const validRequest = {localId: '1234'}; + const validRequest = { localId: '1234' }; expect(() => { return requestValidator(validRequest); }).not.to.throw(); @@ -528,7 +549,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { photoUrl: 'http://www.example.com/1234/photo.png', disableUser: false, phoneNumber: '+11234567890', - customAttributes: JSON.stringify({admin: true, groupId: '123'}), + customAttributes: JSON.stringify({ admin: true, groupId: '123' }), validSince: 1476136676, // Pass an unsupported parameter which should be ignored. ignoreMe: 'bla', @@ -546,9 +567,9 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { }); it('should succeed with valid localId and customAttributes with 1000 char payload', () => { // Test with 1000 characters. - const atLimitClaims = JSON.stringify({key: createRandomString(990)}); + const atLimitClaims = JSON.stringify({ key: createRandomString(990) }); expect(() => { - return requestValidator({localId: '1234', customAttributes: atLimitClaims}); + return requestValidator({ localId: '1234', customAttributes: atLimitClaims }); }).not.to.throw(); }); it('should fail when localId not passed', () => { @@ -561,60 +582,60 @@ 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}`, () => { @@ -622,7 +643,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { // Instantiate custom attributes with invalid claims. const claims: {[key: string]: any} = {}; claims[invalidClaim] = 'bla'; - return requestValidator({localId: '1234', customAttributes: JSON.stringify(claims)}); + return requestValidator({ localId: '1234', customAttributes: JSON.stringify(claims) }); }).to.throw(`Developer claim "${invalidClaim}" is reserved and cannot be specified.`); }); }); @@ -632,12 +653,12 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { sub: 'sub', auth_time: 'time', // eslint-disable-line @typescript-eslint/camelcase }; - return requestValidator({localId: '1234', customAttributes: JSON.stringify(claims)}); - }).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'); }); @@ -646,7 +667,7 @@ describe('FIREBASE_AUTH_SET_ACCOUNT_INFO', () => { describe('responseValidator', () => { const responseValidator = FIREBASE_AUTH_SET_ACCOUNT_INFO.getResponseValidator(); it('should succeed with localId returned', () => { - const validResponse = {localId: '1234'}; + const validResponse = { localId: '1234' }; expect(() => { return responseValidator(validResponse); }).not.to.throw(); @@ -744,57 +765,57 @@ describe('FIREBASE_AUTH_SIGN_UP_NEW_USER', () => { describe('called with invalid parameters', () => { it('should fail with invalid localId', () => { expect(() => { - return requestValidator({localId: ''}); + return requestValidator({ localId: '' }); }).to.throw(); expect(isUidSpy).to.have.been.calledOnce.and.calledWith(''); }); it('should fail with invalid displayName', () => { expect(() => { - return requestValidator({displayName: ['John Doe']}); + return requestValidator({ displayName: ['John Doe'] }); }).to.throw(); }); it('should fail with invalid email', () => { expect(() => { - return requestValidator({email: 'invalid'}); + return requestValidator({ email: 'invalid' }); }).to.throw(); expect(isEmailSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with invalid password', () => { expect(() => { - return requestValidator({password: 'short'}); + return requestValidator({ password: 'short' }); }).to.throw(); expect(isPasswordSpy).to.have.been.calledOnce.and.calledWith('short'); }); it('should fail with invalid emailVerified flag', () => { expect(() => { - return requestValidator({emailVerified: 'yes'}); + return requestValidator({ emailVerified: 'yes' }); }).to.throw(); }); it('should fail with invalid photoUrl', () => { expect(() => { - return requestValidator({photoUrl: 'invalid url'}); + return requestValidator({ photoUrl: 'invalid url' }); }).to.throw(); expect(isUrlSpy).to.have.been.calledOnce.and.calledWith('invalid url'); }); it('should fail with invalid disabled flag', () => { expect(() => { - return requestValidator({disabled: 'no'}); + return requestValidator({ disabled: 'no' }); }).to.throw(); }); it('should fail with invalid phoneNumber', () => { expect(() => { - return requestValidator({phoneNumber: 'invalid'}); + return requestValidator({ phoneNumber: 'invalid' }); }).to.throw(); expect(isPhoneNumberSpy).to.have.been.calledOnce.and.calledWith('invalid'); }); it('should fail with customAttributes', () => { expect(() => { - return requestValidator({customAttributes: JSON.stringify({admin: true})}); + return requestValidator({ customAttributes: JSON.stringify({ admin: true }) }); }).to.throw(); }); it('should fail with validSince', () => { expect(() => { - return requestValidator({validSince: 1476136676}); + return requestValidator({ validSince: 1476136676 }); }).to.throw(); }); }); @@ -802,7 +823,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(); @@ -848,9 +869,13 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { let getTokenStub: sinon.SinonStub; const mockAccessToken: string = utils.generateRandomAccessToken(); const expectedHeaders: {[key: string]: string} = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; + const expectedHeadersEmulator: {[key: string]: string} = { + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, + 'Authorization': 'Bearer owner', + }; const callParams = (path: string, method: any, data: any): HttpRequestConfig => { return { method, @@ -888,6 +913,59 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + describe('Emulator Support', () => { + const method = 'POST'; + const path = handler.path('v1', '/accounts:lookup', 'project_id'); + const expectedResult = utils.responseFrom({ + users : [ + { localId: 'uid' }, + ], + }); + const data = { localId: ['uid'] }; + + after(() => { + delete process.env.FIREBASE_AUTH_EMULATOR_HOST; + }) + + it('should call a prod URL with a real token when emulator is not running', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `https://${host}${path}`, + data, + headers: expectedHeaders, + timeout, + }); + }); + }); + + it('should call a local URL with a mock token when the emulator is running', () => { + const emulatorHost = 'localhost:9099'; + process.env.FIREBASE_AUTH_EMULATOR_HOST = emulatorHost; + + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getAccountInfoByUid('uid') + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method, + url: `http://${emulatorHost}/identitytoolkit.googleapis.com${path}`, + data, + headers: expectedHeadersEmulator, + timeout, + }); + }); + }); + }); + describe('createSessionCookie', () => { const durationInMs = 24 * 60 * 60 * 1000; const path = handler.path('v1', ':createSessionCookie', 'project_id'); @@ -897,7 +975,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ sessionCookie: 'SESSION_COOKIE', }); - const data = {idToken: 'ID_TOKEN', validDuration: durationInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -913,7 +991,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { sessionCookie: 'SESSION_COOKIE', }); const durationAtLimitInMs = 14 * 24 * 60 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -929,7 +1007,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { sessionCookie: 'SESSION_COOKIE', }); const durationAtLimitInMs = 5 * 60 * 1000; - const data = {idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000}; + const data = { idToken: 'ID_TOKEN', validDuration: durationAtLimitInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -950,7 +1028,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given an invalid duration', () => { @@ -963,7 +1041,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given a duration less than minimum allowed', () => { @@ -977,7 +1055,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given a duration greater than maximum allowed', () => { @@ -991,7 +1069,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected when the backend returns an error', () => { @@ -1001,7 +1079,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); - const data = {idToken: 'invalid-token', validDuration: durationInMs / 1000}; + const data = { idToken: 'invalid-token', validDuration: durationInMs / 1000 }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1010,7 +1088,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1022,10 +1100,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given a valid email', () => { const expectedResult = utils.responseFrom({ users : [ - {email: 'user@example.com'}, + { email: 'user@example.com' }, ], }); - const data = {email: ['user@example.com']}; + const data = { email: ['user@example.com'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1047,7 +1125,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { kind: 'identitytoolkit#GetAccountInfoResponse', }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {email: ['user@example.com']}; + const data = { email: ['user@example.com'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1056,7 +1134,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1068,10 +1146,10 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given a valid localId', () => { const expectedResult = utils.responseFrom({ users : [ - {localId: 'uid'}, + { localId: 'uid' }, ], }); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1087,7 +1165,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { kind: 'identitytoolkit#GetAccountInfoResponse', }); const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1096,7 +1174,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1107,7 +1185,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: ['uid']}; + const data = { localId: ['uid'] }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1117,7 +1195,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1167,7 +1245,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.not.been.called; }); @@ -1189,7 +1267,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1199,7 +1277,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when given more than 100 identifiers', () => { const identifiers: UserIdentifier[] = []; for (let i = 0; i < 101; i++) { - identifiers.push({uid: 'id' + i}); + identifiers.push({ uid: 'id' + i }); } const requestHandler = handler.init(mockApp); @@ -1212,7 +1290,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.getAccountInfoByIdentifiers([]) .then((getUsersResult) => { - expect(getUsersResult).to.deep.equal({users: []}); + expect(getUsersResult).to.deep.equal({ users: [] }); }); }); @@ -1222,7 +1300,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { stubs.push(stub); const requestHandler = handler.init(mockApp); - const notFoundIds = [{uid: 'id that doesnt exist'}]; + const notFoundIds = [{ uid: 'id that doesnt exist' }]; return requestHandler.getAccountInfoByIdentifiers(notFoundIds) .then((getUsersResult) => { expect(getUsersResult).to.deep.equal({ users: [] }); @@ -1231,39 +1309,39 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when given an invalid uid', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{uid: 'too long ' + ('.' as any).repeat(128)}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ uid: 'too long ' + ('.' as any).repeat(128) }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-uid'); }); it('should throw when given an invalid email', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{email: 'invalid email addr'}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ email: 'invalid email addr' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-email'); }); it('should throw when given an invalid phone number', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{phoneNumber: 'invalid phone number'}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ phoneNumber: 'invalid phone number' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-phone-number'); }); it('should throw when given an invalid provider', () => { const requestHandler = handler.init(mockApp); - expect(() => requestHandler.getAccountInfoByIdentifiers([{providerUid: '', providerId: ''}])) + expect(() => requestHandler.getAccountInfoByIdentifiers([{ providerUid: '', providerId: '' }])) .to.throw(FirebaseAuthError) .with.property('code', 'auth/invalid-provider-id'); }); it('should throw when given a single bad identifier', () => { const identifiers: UserIdentifier[] = [ - {uid: 'valid_id1'}, - {uid: 'valid_id2'}, - {uid: 'invalid id; too long. ' + ('.' as any).repeat(128)}, - {uid: 'valid_id4'}, - {uid: 'valid_id5'}, + { uid: 'valid_id1' }, + { uid: 'valid_id2' }, + { uid: 'invalid id; too long. ' + ('.' as any).repeat(128) }, + { uid: 'valid_id4' }, + { uid: 'valid_id5' }, ]; const requestHandler = handler.init(mockApp); @@ -1347,15 +1425,9 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { factorId: 'phone', enrollmentTime: new Date().toUTCString(), }, - { - uid: 'mfaUid2', - phoneNumber: '+16505550002', - displayName: 'Personal phone number', - factorId: 'phone', - }, ], }, - customClaims: {admin: true}, + customClaims: { admin: true }, // Tenant ID accepted on user batch upload. tenantId, }, @@ -1365,7 +1437,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { passwordHash: Buffer.from('userpass'), passwordSalt: Buffer.from('NaCl'), }, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '5678', phoneNumber: '+16505550101' }, ]; const options = { hash: { @@ -1376,7 +1448,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw on invalid options without making an underlying API call', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `Unsupported hash algorithm provider "invalid".`, + 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { hash: { @@ -1396,7 +1468,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should throw when 1001 UserImportRecords are provided', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.MAXIMUM_USER_COUNT_EXCEEDED, - `A maximum of 1000 users can be imported at once.`, + 'A maximum of 1000 users can be imported at once.', ); const stub = sinon.stub(HttpClient.prototype, 'send'); stubs.push(stub); @@ -1489,8 +1561,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should resolve with expected result on underlying API partial succcess', () => { const expectedResult = utils.responseFrom({ error: [ - {index: 0, message: 'Some error occurred'}, - {index: 1, message: 'Another error occurred'}, + { index: 0, message: 'Some error occurred' }, + { index: 1, message: 'Another error occurred' }, ], }); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); @@ -1500,7 +1572,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const userImportBuilder = new UserImportBuilder(users, options); return requestHandler.uploadAccount(users, options) .then((result) => { - expect(result).to.deep.equal(userImportBuilder.buildResponse(expectedResult.data.error)); + expectUserImportResult( + result, userImportBuilder.buildResponse(expectedResult.data.error)); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, userImportBuilder.buildRequest())); }); @@ -1509,15 +1582,15 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should resolve without underlying API call when users are processed client side', () => { // These users should fail to upload due to invalid phone number and email fields. const testUsers = [ - {uid: '1234', phoneNumber: 'invalid'}, - {uid: '5678', email: 'invalid'}, + { uid: '1234', phoneNumber: 'invalid' }, + { uid: '5678', email: 'invalid' }, ] as any; const expectedResult = { successCount: 0, failureCount: 2, errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, ], }; const stub = sinon.stub(HttpClient.prototype, 'send'); @@ -1526,40 +1599,40 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.uploadAccount(testUsers) .then((result) => { - expect(result).to.deep.equal(expectedResult); + expectUserImportResult(result, expectedResult); expect(stub).to.have.not.been.called; }); }); it('should validate underlying users and resolve with expected errors', () => { const testUsers = [ - {uid: 'user1', displayName: false}, - {uid: 123}, - {uid: 'user2', email: 'invalid'}, - {uid: 'user3', phoneNumber: 'invalid'}, - {uid: 'user4', emailVerified: 'invalid'}, - {uid: 'user5', photoURL: 'invalid'}, - {uid: 'user6', disabled: 'invalid'}, - {uid: 'user7', metadata: {creationTime: 'invalid'}}, - {uid: 'user8', metadata: {lastSignInTime: 'invalid'}}, - {uid: 'user9', customClaims: {admin: true, aud: 'bla'}}, - {uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid'}, - {uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid'}, - {uid: 'user12', providerData: [{providerId: 'google.com'}]}, + { uid: 'user1', displayName: false }, + { uid: 123 }, + { uid: 'user2', email: 'invalid' }, + { uid: 'user3', phoneNumber: 'invalid' }, + { uid: 'user4', emailVerified: 'invalid' }, + { uid: 'user5', photoURL: 'invalid' }, + { uid: 'user6', disabled: 'invalid' }, + { uid: 'user7', metadata: { creationTime: 'invalid' } }, + { uid: 'user8', metadata: { lastSignInTime: 'invalid' } }, + { uid: 'user9', customClaims: { admin: true, aud: 'bla' } }, + { uid: 'user10', email: 'user10@example.com', passwordHash: 'invalid' }, + { uid: 'user11', email: 'user11@example.com', passwordSalt: 'invalid' }, + { uid: 'user12', providerData: [{ providerId: 'google.com' }] }, { uid: 'user13', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', displayName: false}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', displayName: false }], }, { uid: 'user14', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', email: 'invalid'}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', email: 'invalid' }], }, { uid: 'user15', - providerData: [{providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid'}], + providerData: [{ providerId: 'google.com', uid: 'RAW_ID', photoURL: 'invalid' }], }, - {uid: 'user16', providerData: [{}]}, - {email: 'user17@example.com'}, + { uid: 'user16', providerData: [{}] }, + { email: 'user17@example.com' }, { uid: 'user18', email: 'user18@example.com', @@ -1644,81 +1717,81 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { successCount: 0, failureCount: testUsers.length, errors: [ - {index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME)}, - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, - {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, - {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, - {index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED)}, - {index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL)}, - {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD)}, - {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME)}, - {index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME)}, + { index: 0, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISPLAY_NAME) }, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, + { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, + { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, + { index: 4, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL_VERIFIED) }, + { index: 5, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHOTO_URL) }, + { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_DISABLED_FIELD) }, + { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_CREATION_TIME) }, + { index: 8, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_LAST_SIGN_IN_TIME) }, { index: 9, error: new FirebaseAuthError( AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, + 'Developer claim "aud" is reserved and cannot be specified.', ), }, - {index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, - {index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, + { index: 10, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 11, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 12, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The provider "uid" for "google.com" must be a valid non-empty string.`, + 'The provider "uid" for "google.com" must be a valid non-empty string.', ), }, { index: 13, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The provider "displayName" for "google.com" must be a valid string.`, + 'The provider "displayName" for "google.com" must be a valid string.', ), }, { index: 14, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_EMAIL, - `The provider "email" for "google.com" must be a valid email string.`, + 'The provider "email" for "google.com" must be a valid email string.', ), }, { index: 15, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHOTO_URL, - `The provider "photoURL" for "google.com" must be a valid URL string.`, + 'The provider "photoURL" for "google.com" must be a valid URL string.', ), }, - {index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID)}, - {index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID)}, + { index: 16, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PROVIDER_ID) }, + { index: 17, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_UID) }, { index: 18, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ), }, { index: 19, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "mfaUid1" must be a valid string.`, + 'The second factor "displayName" for "mfaUid1" must be a valid string.', ), }, { index: 20, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "mfaUid2" must be a valid UTC date string.`, + 'The second factor "enrollmentTime" for "mfaUid2" must be a valid UTC date string.', ), }, { index: 21, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "mfaUid3" must be a non-empty ` + - `E.164 standard compliant identifier string.`, + 'The second factor "phoneNumber" for "mfaUid3" must be a non-empty ' + + 'E.164 standard compliant identifier string.', ), }, { @@ -1736,7 +1809,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.uploadAccount(testUsers, validOptions) .then((result) => { - expect(result).to.deep.equal(expectedResult); + expectUserImportResult(result, expectedResult); expect(stub).to.have.not.been.called; }); }); @@ -1749,7 +1822,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); @@ -1761,12 +1834,11 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, userImportBuilder.buildRequest())); }); }); - }); describe('downloadAccount', () => { @@ -1776,8 +1848,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 500; const expectedResult = utils.responseFrom({ users : [ - {localId: 'uid1'}, - {localId: 'uid2'}, + { localId: 'uid1' }, + { localId: 'uid2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -1808,7 +1880,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.downloadAccount(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({users: []}); + expect(result).to.deep.equal({ users: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1830,8 +1902,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 1000.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 1000.', ); const requestHandler = handler.init(mockApp); @@ -1839,7 +1911,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected given an invalid next page token', () => { @@ -1852,7 +1924,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected when the backend returns an error', () => { @@ -1874,7 +1946,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1887,7 +1959,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom({ kind: 'identitytoolkit#DeleteAccountResponse', }); - const data = {localId: 'uid'}; + const data = { localId: 'uid' }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1905,7 +1977,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, }); const expectedError = FirebaseAuthError.fromServerError('OPERATION_NOT_ALLOWED'); - const data = {localId: 'uid'}; + const data = { localId: 'uid' }; const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); stubs.push(stub); @@ -1914,7 +1986,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -1953,7 +2025,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be fulfilled given valid uids', async () => { const expectedResult = utils.responseFrom({}); - const data = {localIds: ['uid1', 'uid2', 'uid3'], force: true}; + const data = { localIds: ['uid1', 'uid2', 'uid3'], force: true }; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -1993,6 +2065,11 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { phoneNumber: '+16505551000', factorId: 'phone', } as UpdateMultiFactorInfoRequest, + { + // No error should be thrown when no uid is specified. + phoneNumber: '+16505551234', + factorId: 'phone', + } as UpdateMultiFactorInfoRequest, ], }, }; @@ -2018,6 +2095,9 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { mfaEnrollmentId: 'enrolledSecondFactor2', phoneInfo: '+16505551000', }, + { + phoneInfo: '+16505551234', + }, ], }, }; @@ -2080,7 +2160,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { expect(returnedUid).to.be.equal(uid); // Confirm expected rpc request parameters sent. expect(stub).to.have.been.calledOnce.and.calledWith( - callParams(path, method, {localId: uid})); + callParams(path, method, { localId: uid })); }); }); @@ -2161,7 +2241,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send update request to delete enrolled factors. - return requestHandler.updateExistingAccount(uid, {multiFactor: {enrolledFactors: null}}) + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: null } }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2183,7 +2263,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send update request to delete enrolled factors. - return requestHandler.updateExistingAccount(uid, {multiFactor: {enrolledFactors: []}}) + return requestHandler.updateExistingAccount(uid, { multiFactor: { enrolledFactors: [] } }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2204,7 +2284,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2219,7 +2299,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor uid', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_UID, - `The second factor "uid" must be a valid non-empty string.`, + 'The second factor "uid" must be a valid non-empty string.', ), secondFactor: { uid: ['enrollmentId'], @@ -2232,7 +2312,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor display name', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.`, + 'The second factor "displayName" for "enrolledSecondFactor1" must be a valid string.', ), secondFactor: { uid: 'enrolledSecondFactor1', @@ -2245,8 +2325,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor phone number', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "enrolledSecondFactor1" must be a non-empty ` + - `E.164 standard compliant identifier string.`), + 'The second factor "phoneNumber" for "enrolledSecondFactor1" must be a non-empty ' + + 'E.164 standard compliant identifier string.'), secondFactor: { uid: 'enrolledSecondFactor1', phoneNumber: 'invalid', @@ -2258,8 +2338,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor enrollment time', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "enrolledSecondFactor1" must be a valid ` + - `UTC date string.`), + 'The second factor "enrollmentTime" for "enrolledSecondFactor1" must be a valid ' + + 'UTC date string.'), secondFactor: { uid: 'enrolledSecondFactor1', phoneNumber: '+16505557348', @@ -2289,7 +2369,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected error should be thrown. - expect(error).to.deep.equal(invalidSecondFactorTest.error); + expect(error).to.deep.include(invalidSecondFactorTest.error); }); }); }); @@ -2309,7 +2389,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2323,7 +2403,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2344,7 +2424,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2355,7 +2435,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = handler.path('v1', '/accounts:update', 'project_id'); const method = 'POST'; const uid = '12345678'; - const claims = {admin: true, groupId: '1234'}; + const claims = { admin: true, groupId: '1234' }; const expectedValidData = { localId: uid, customAttributes: JSON.stringify(claims), @@ -2412,7 +2492,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2429,7 +2509,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2437,17 +2517,17 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { // Expected error when invalid claims are provided. const expectedError = new FirebaseAuthError( AuthClientErrorCode.FORBIDDEN_CLAIM, - `Developer claim "aud" is reserved and cannot be specified.`, + 'Developer claim "aud" is reserved and cannot be specified.', ); const requestHandler = handler.init(mockApp); - const blacklistedClaims = {admin: true, aud: 'bla'}; + 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.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2467,7 +2547,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2513,7 +2593,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid uid', () => { const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_UID); - const invalidUid: any = {localId: uid}; + const invalidUid: any = { localId: uid }; const requestHandler = handler.init(mockApp); return requestHandler.revokeRefreshTokens(invalidUid as any) @@ -2521,7 +2601,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid uid error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2547,7 +2627,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -2623,7 +2703,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send empty create new account request with only a uid provided. - return requestHandler.createNewAccount({uid}) + return requestHandler.createNewAccount({ uid }) .then((returnedUid: string) => { // uid should be returned. expect(returnedUid).to.be.equal(uid); @@ -2662,7 +2742,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2679,7 +2759,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); // Send create new account request with no enrolled factors. - const request: any = {uid, multiFactor: {enrolledFactors: null}}; + const request: any = { uid, multiFactor: { enrolledFactors: null } }; return requestHandler.createNewAccount(request) .then((returnedUid: string) => { // uid should be returned. @@ -2716,7 +2796,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor display name', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_DISPLAY_NAME, - `The second factor "displayName" for "+16505557348" must be a valid string.`, + 'The second factor "displayName" for "+16505557348" must be a valid string.', ), secondFactor: { phoneNumber: '+16505557348', @@ -2728,8 +2808,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { name: 'invalid second factor phone number', error: new FirebaseAuthError( AuthClientErrorCode.INVALID_PHONE_NUMBER, - `The second factor "phoneNumber" for "invalid" must be a non-empty ` + - `E.164 standard compliant identifier string.`), + 'The second factor "phoneNumber" for "invalid" must be a non-empty ' + + 'E.164 standard compliant identifier string.'), secondFactor: { phoneNumber: 'invalid', displayName: 'Spouse\'s phone number', @@ -2773,7 +2853,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected error should be thrown. - expect(error).to.deep.equal(invalidSecondFactorTest.error); + expect(error).to.deep.include(invalidSecondFactorTest.error); }); }); }); @@ -2793,7 +2873,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2807,7 +2887,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Expected invalid phone number error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2829,7 +2909,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2853,7 +2933,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2876,7 +2956,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -2945,7 +3025,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2959,7 +3039,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -2980,7 +3060,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, method, expectedValidData)); }); @@ -3090,7 +3170,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid email error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3098,7 +3178,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const invalidRequestType = 'invalid'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `"invalid" is not a supported email action request type.`, + '"invalid" is not a supported email action request type.', ); const requestHandler = handler.init(mockApp); @@ -3107,7 +3187,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3124,7 +3204,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { throw new Error('Unexpected success'); }, (error) => { // Invalid argument error should be thrown. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3139,7 +3219,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }, expectedActionCodeSettingsRequest); // Simulate response missing link. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({email})); + .resolves(utils.responseFrom({ email })); stubs.push(stub); const requestHandler = handler.init(mockApp); @@ -3147,7 +3227,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -3173,7 +3253,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); }); }); @@ -3211,7 +3291,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3231,7 +3311,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, {})); }); @@ -3245,8 +3325,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 50; const expectedResult = utils.responseFrom({ oauthIdpConfigs : [ - {name: 'projects/project1/oauthIdpConfigs/oidc.provider1'}, - {name: 'projects/project1/oauthIdpConfigs/oidc.provider2'}, + { name: 'projects/project1/oauthIdpConfigs/oidc.provider1' }, + { name: 'projects/project1/oauthIdpConfigs/oidc.provider2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -3279,7 +3359,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.listOAuthIdpConfigs(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({oauthIdpConfigs: []}); + expect(result).to.deep.equal({ oauthIdpConfigs: [] }); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, data)); }); @@ -3305,8 +3385,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 100.', ); const requestHandler = handler.init(mockApp); @@ -3314,7 +3394,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3328,7 +3408,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3351,7 +3431,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, data)); }); @@ -3388,7 +3468,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3408,7 +3488,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, {})); }); @@ -3419,6 +3499,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const providerId = 'oidc.provider'; const path = handler.path('v2', `/oauthIdpConfigs?oauthIdpConfigId=${providerId}`, 'project_id'); const expectedHttpMethod = 'POST'; + const clientSecret = 'CLIENT_SECRET'; + const responseType = { code: true }; const configOptions = { providerId, displayName: 'OIDC_DISPLAY_NAME', @@ -3435,6 +3517,26 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const expectedResult = utils.responseFrom(deepExtend({ name: `projects/project1/oauthIdpConfigs/${providerId}`, }, expectedRequest)); + const expectedCodeFlowOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowResult = utils.responseFrom(deepExtend({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }, expectedCodeFlowRequest)); it('should be fulfilled given valid parameters', () => { const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); @@ -3449,6 +3551,19 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given valid parameters for OIDC code flow', () => { + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedCodeFlowResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.createOAuthIdpConfig(expectedCodeFlowOptions) + .then((response) => { + expect(response).to.deep.equal(expectedCodeFlowResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(path, expectedHttpMethod, expectedCodeFlowRequest)); + }); + }); + it('should be rejected given invalid parameters', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_CONFIG, @@ -3462,7 +3577,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3479,7 +3594,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -3500,7 +3615,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -3511,6 +3626,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const providerId = 'oidc.provider'; const path = handler.path('v2', `/oauthIdpConfigs/${providerId}`, 'project_id'); const expectedHttpMethod = 'PATCH'; + const clientSecret = 'CLIENT_SECRET'; + const responseType = { code: true }; const configOptions = { displayName: 'OIDC_DISPLAY_NAME', enabled: true, @@ -3534,6 +3651,26 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { clientId: 'NEW_CLIENT_ID', issuer: 'https://oidc.com/issuer2', })); + const expectedCodeFlowOptions = { + providerId, + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowRequest = { + displayName: 'OIDC_DISPLAY_NAME', + enabled: true, + clientId: 'CLIENT_ID', + issuer: 'https://oidc.com/issuer', + clientSecret, + responseType, + }; + const expectedCodeFlowResult = utils.responseFrom(deepExtend({ + name: `projects/project1/oauthIdpConfigs/${providerId}`, + }, expectedCodeFlowRequest)); it('should be fulfilled given full parameters', () => { const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId'; @@ -3549,6 +3686,20 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given full parameters for OIDC code flow', () => { + const expectedPath = path + '?updateMask=enabled,displayName,issuer,clientId,clientSecret,responseType.code'; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedCodeFlowResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.updateOAuthIdpConfig(providerId, expectedCodeFlowOptions) + .then((response) => { + expect(response).to.deep.equal(expectedCodeFlowResult.data); + expect(stub).to.have.been.calledOnce.and.calledWith( + callParams(expectedPath, expectedHttpMethod, expectedCodeFlowRequest)); + }); + }); + it('should be fulfilled given partial parameters', () => { const expectedPath = path + '?updateMask=enabled,clientId'; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedPartialResult); @@ -3607,7 +3758,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3625,7 +3776,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3643,7 +3794,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -3665,7 +3816,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -3704,7 +3855,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3724,7 +3875,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); }); }); @@ -3737,8 +3888,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 50; const expectedResult = utils.responseFrom({ inboundSamlConfigs : [ - {name: 'projects/project1/inboundSamlConfigs/saml.provider1'}, - {name: 'projects/project1/inboundSamlConfigs/saml.provider2'}, + { name: 'projects/project1/inboundSamlConfigs/saml.provider1' }, + { name: 'projects/project1/inboundSamlConfigs/saml.provider2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -3770,7 +3921,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp); return requestHandler.listInboundSamlConfigs(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({inboundSamlConfigs: []}); + expect(result).to.deep.equal({ inboundSamlConfigs: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); @@ -3794,8 +3945,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive integer that does not ` + - `exceed 100.`, + 'Required "maxResults" must be a positive integer that does not ' + + 'exceed 100.', ); const requestHandler = handler.init(mockApp); @@ -3803,7 +3954,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3817,7 +3968,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3840,7 +3991,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, data)); }); }); @@ -3875,7 +4026,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -3895,7 +4046,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, expectedHttpMethod, {})); }); }); @@ -3922,8 +4073,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -3963,7 +4114,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -3980,7 +4131,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -4001,7 +4152,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(path, expectedHttpMethod, expectedRequest)); }); @@ -4029,8 +4180,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -4051,8 +4202,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ssoUrl: 'https://example.com/login2', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -4151,7 +4302,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4169,7 +4320,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4187,7 +4338,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -4209,7 +4360,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, expectedHttpMethod, expectedRequest)); }); @@ -4232,7 +4383,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.getTenant(tenantId) .then((result) => { - expect(result).to.deep.equal(expectedResult.data); + expect(result).to.deep.include(expectedResult.data); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4247,7 +4398,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4267,7 +4418,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4280,8 +4431,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const maxResults = 500; const expectedResult = utils.responseFrom({ tenants : [ - {name: 'projects/project_id/tenants/tenant-id1'}, - {name: 'projects/project_id/tenants/tenant-id2'}, + { name: 'projects/project_id/tenants/tenant-id1' }, + { name: 'projects/project_id/tenants/tenant-id2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }); @@ -4313,7 +4464,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const requestHandler = handler.init(mockApp) as AuthRequestHandler; return requestHandler.listTenants(maxResults, nextPageToken) .then((result) => { - expect(result).to.deep.equal({tenants: []}); + expect(result).to.deep.equal({ tenants: [] }); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -4337,8 +4488,8 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { it('should be rejected given an invalid maxResults', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, - `Required "maxResults" must be a positive non-zero number that does not ` + - `exceed the allowed 1000.`, + 'Required "maxResults" must be a positive non-zero number that does not ' + + 'exceed the allowed 1000.', ); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4346,7 +4497,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4360,7 +4511,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4383,7 +4534,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, data)); }); }); @@ -4417,7 +4568,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4437,7 +4588,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, {})); }); }); @@ -4446,17 +4597,33 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { describe('createTenant', () => { const path = '/v2/projects/project_id/tenants'; const postMethod = 'POST'; - const tenantOptions: TenantOptions = { + const tenantOptions: CreateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, 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', @@ -4487,7 +4654,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -4504,7 +4671,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4516,7 +4683,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { ); // Resource name should have /tenants/tenant-id in path. This should throw an error. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({name: 'projects/project_id'})); + .resolves(utils.responseFrom({ name: 'projects/project_id' })); stubs.push(stub); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4524,7 +4691,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4537,7 +4704,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); @@ -4548,7 +4715,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, postMethod, expectedRequest)); }); }); @@ -4558,24 +4725,41 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = '/v2/projects/project_id/tenants/tenant-id'; const patchMethod = 'PATCH'; const tenantId = 'tenant-id'; - const tenantOptions = { + const tenantOptions: 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'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); @@ -4595,7 +4779,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { allowPasswordSignup: true, }; const partialTenantOptions = { - emailSignInConfig: {enabled: true}, + emailSignInConfig: { enabled: true }, }; stubs.push(stub); @@ -4638,7 +4822,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); }); @@ -4656,12 +4840,13 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); it('should be rejected when the backend returns a response missing name', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', @@ -4674,21 +4859,22 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); }); it('should be rejected when the backend returns a response missing tenant ID in response name', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, 'INTERNAL ASSERT FAILED: Unable to update tenant', ); // Resource name should have /tenants/tenant-id in path. This should throw an error. const stub = sinon.stub(HttpClient.prototype, 'send') - .resolves(utils.responseFrom({name: 'projects/project_id'})); + .resolves(utils.responseFrom({ name: 'projects/project_id' })); stubs.push(stub); const requestHandler = handler.init(mockApp) as AuthRequestHandler; @@ -4696,14 +4882,15 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); }); it('should be rejected when the backend returns an error', () => { - const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName'; + const expectedPath = path + '?updateMask=allowPasswordSignup,enableEmailLinkSignin,displayName,' + + 'mfaConfig.state,mfaConfig.enabledProviders,testPhoneNumbers'; const expectedServerError = utils.errorFrom({ error: { message: 'INTERNAL_ERROR', @@ -4711,7 +4898,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); const expectedError = new FirebaseAuthError( AuthClientErrorCode.INTERNAL_ERROR, - `An internal error has occurred. Raw server response: ` + + 'An internal error has occurred. Raw server response: ' + `"${JSON.stringify(expectedServerError.response.data)}"`, ); const stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedServerError); @@ -4722,7 +4909,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .then(() => { throw new Error('Unexpected success'); }, (error) => { - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); expect(stub).to.have.been.calledOnce.and.calledWith( callParams(expectedPath, patchMethod, expectedRequest)); }); diff --git a/test/unit/auth/auth-config.spec.ts b/test/unit/auth/auth-config.spec.ts old mode 100755 new mode 100644 index d5777d787d..d9e6ce4abc --- a/test/unit/auth/auth-config.spec.ts +++ b/test/unit/auth/auth-config.spec.ts @@ -19,15 +19,20 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {deepCopy} from '../../../src/utils/deep-copy'; +import { deepCopy } from '../../../src/utils/deep-copy'; import { OIDCConfig, SAMLConfig, SAMLConfigServerRequest, SAMLConfigServerResponse, OIDCConfigServerRequest, - OIDCConfigServerResponse, SAMLUpdateAuthProviderRequest, - OIDCUpdateAuthProviderRequest, SAMLAuthProviderConfig, OIDCAuthProviderConfig, - EmailSignInConfig, + OIDCConfigServerResponse, + EmailSignInConfig, MultiFactorAuthConfig, validateTestPhoneNumbers, + MAXIMUM_TEST_PHONE_NUMBERS, } from '../../../src/auth/auth-config'; +import { auth } from '../../../src/auth/index'; +import SAMLUpdateAuthProviderRequest = auth.SAMLUpdateAuthProviderRequest; +import OIDCUpdateAuthProviderRequest = auth.OIDCUpdateAuthProviderRequest; +import SAMLAuthProviderConfig = auth.SAMLAuthProviderConfig; +import OIDCAuthProviderConfig = auth.OIDCAuthProviderConfig; chai.should(); chai.use(sinonChai); @@ -152,6 +157,216 @@ describe('EmailSignInConfig', () => { }); }); +describe('MultiFactorAuthConfig', () => { + describe('constructor', () => { + const validConfig = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + + it('should throw on missing state', () => { + expect(() => new MultiFactorAuthConfig({ + enabledProviders: ['PHONE_SMS'], + } as any)).to.throw('INTERNAL ASSERT FAILED: Invalid multi-factor configuration response'); + }); + + it('should set readonly property "state" to ENABLED on state enabled', () => { + expect(validConfig.state).to.equals('ENABLED'); + }); + + it('should set readonly property "state" to DISABLED on state disabled', () => { + const disabledState = new MultiFactorAuthConfig({ + state: 'DISABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(disabledState.state).to.equals('DISABLED'); + }); + + it('should set readonly property "factorIds"', () => { + expect(validConfig.factorIds).to.deep.equal(['phone']); + }); + + it('should ignore unsupported backend types if found', () => { + const unsupportedType = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['UNSUPPORTED_TYPE', 'PHONE_SMS'], + } as any); + expect(unsupportedType.factorIds).to.deep.equal(['phone']); + }); + + it('should return empty factorIds array if no supported types are found', () => { + const unsupportedType = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['UNSUPPORTED_TYPE'], + } as any); + expect(unsupportedType.factorIds).to.deep.equal([]); + }); + }); + + describe('toJSON()', () => { + it('should return expected JSON representation', () => { + const config = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(config.toJSON()).to.deep.equal({ + state: 'ENABLED', + factorIds: ['phone'], + }); + }); + }); + + describe('buildServerRequest()', () => { + it('should return expected server request on valid state and factorIds', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: ['phone'], + })).to.deep.equal({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + }); + + it('should return expected server request on valid state without factorIds', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + })).to.deep.equal({ + state: 'DISABLED', + }); + }); + + it('should return empty enabledProviders when an empty "options.factorIds" is provided', () => { + expect(MultiFactorAuthConfig.buildServerRequest({ + state: 'DISABLED', + factorIds: [], + })).to.deep.equal({ + state: 'DISABLED', + enabledProviders: [], + }); + }); + + const invalidOptions = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; + invalidOptions.forEach((options) => { + it('should throw on invalid MultiFactorAuthConfig:' + JSON.stringify(options), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest(options as any); + }).to.throw('"MultiFactorConfig" must be a non-null object.'); + }); + }); + + it('should throw on MultiFactorAuthConfig with unsupported attribute', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + unsupported: true, + state: 'ENABLED', + factorIds: ['phone'], + } as any); + }).to.throw('"unsupported" is not a valid MultiFactorConfig parameter.'); + }); + + const invalidState = [ + null, NaN, 0, 1, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, true, false, + ]; + invalidState.forEach((state) => { + it('should throw on invalid MultiFactorConfig.state:' + JSON.stringify(state), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state, + factorIds: ['phone'], + } as any); + }).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".'); + }); + }); + + it('should throw on non-array MultiFactorAuthConfig.factorIds', () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: 'phone', + } as any); + }).to.throw('"MultiFactorConfig.factorIds" must be an array of valid "AuthFactorTypes".'); + }); + + const invalidFactorIds = invalidState; + invalidFactorIds.forEach((factorId) => { + it('should throw on invalid MultiFactorConfig.factorIds:' + JSON.stringify(factorId), () => { + expect(() => { + MultiFactorAuthConfig.buildServerRequest({ + state: 'ENABLED', + factorIds: [factorId], + } as any); + }).to.throw(`"${factorId}" is not a valid "AuthFactorType".`); + }); + }); + }); +}); + +describe('validateTestPhoneNumbers', () => { + it('should not throw an error on empty object', () => { + expect(() => validateTestPhoneNumbers({})).not.to.throw(); + }); + + it('should not throw an error on valid phone number / code pairs', () => { + const pairs = { + '+16505551234': '019287', + '+16505550676': '985235', + '+1 (123) 456-7890': '098765', + '+1 800 FLOwerS': '000000', + }; + + expect(() => validateTestPhoneNumbers(pairs)).not.to.throw(); + }); + + it(`should not throw when ${MAXIMUM_TEST_PHONE_NUMBERS} pairs are provided`, () => { + const pairs: {[key: string]: string} = {}; + for (let i = 0; i < MAXIMUM_TEST_PHONE_NUMBERS; i++) { + pairs[`+1650555${'0'.repeat(4 - i.toString().length)}${i}`] = '012938'; + } + + expect(() => validateTestPhoneNumbers(pairs)).not.to.throw(); + }); + + it(`should throw when >${MAXIMUM_TEST_PHONE_NUMBERS} pairs are provided`, () => { + const pairs: {[key: string]: string} = {}; + for (let i = 0; i < MAXIMUM_TEST_PHONE_NUMBERS + 1; i++) { + pairs[`+1650555${'0'.repeat(4 - i.toString().length)}${i}`] = '012938'; + } + + expect(() => validateTestPhoneNumbers(pairs)).to.throw(); + }); + + const nonObjects = [NaN, 0, 1, true, false, '', 'a', _.noop]; + nonObjects.forEach((nonObject) => { + it(`should throw when non-object ${JSON.stringify(nonObject)} is provided`, () => { + expect(() => validateTestPhoneNumbers(nonObject as any)).to.throw(); + }); + }); + + const invalidPhoneNumbers = [ + null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop, '+', '+ ()-', + ]; + invalidPhoneNumbers.forEach((invalidPhoneNumber) => { + it(`should throw when "${JSON.stringify(invalidPhoneNumber)}" is used as phone number`, () => { + const pairs = { + [invalidPhoneNumber as any]: '123456', + }; + expect(() => validateTestPhoneNumbers(pairs)).to.throw(); + }); + }); + + const invalidCodes = [ + NaN, 0, 1, true, false, '', 'a', _.noop, '12345', '1234567', '123a56', '12 345', 123456, + ]; + invalidCodes.forEach((invalidCode) => { + it(`should throw when an invalid code ${JSON.stringify(invalidCode)} is provided`, () => { + const pairs = { + '+16505551234': invalidCode, + }; + expect(() => validateTestPhoneNumbers(pairs as any)).to.throw(); + }); + }); +}); + describe('SAMLConfig', () => { const serverRequest: SAMLConfigServerRequest = { idpConfig: { @@ -159,8 +374,8 @@ describe('SAMLConfig', () => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -177,8 +392,8 @@ describe('SAMLConfig', () => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -195,7 +410,6 @@ describe('SAMLConfig', () => { x509Certificates: ['CERT1', 'CERT2'], rpEntityId: 'RP_ENTITY_ID', callbackURL: 'https://projectId.firebaseapp.com/__/auth/handler', - enableRequestSigning: true, enabled: true, displayName: 'samlProviderName', }; @@ -314,7 +528,9 @@ describe('SAMLConfig', () => { describe('buildServerRequest()', () => { it('should return expected server request on valid input', () => { - expect(SAMLConfig.buildServerRequest(clientRequest)).to.deep.equal(serverRequest); + const request = deepCopy(clientRequest); + (request as any).enableRequestSigning = true; + expect(SAMLConfig.buildServerRequest(request)).to.deep.equal(serverRequest); }); it('should ignore missing fields if not required', () => { @@ -347,7 +563,7 @@ describe('SAMLConfig', () => { .to.throw('"SAMLAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "saml.".'); }); - const nonAuthConfigOptions = [null, undefined, {}, {other: 'value'}]; + const nonAuthConfigOptions = [null, undefined, {}, { other: 'value' }]; nonAuthConfigOptions.forEach((nonAuthConfig) => { it('should return null when no AuthConfig is provided: ' + JSON.stringify(nonAuthConfig), () => { expect(SAMLConfig.buildServerRequest(nonAuthConfig as any)) @@ -409,7 +625,7 @@ describe('SAMLConfig', () => { const invalidClientRequest = deepCopy(clientRequest) as any; invalidClientRequest.unsupported = 'value'; expect(() => SAMLConfig.validate(invalidClientRequest)) - .to.throw(`"unsupported" is not a valid SAML config parameter.`); + .to.throw('"unsupported" is not a valid SAML config parameter.'); }); const invalidProviderIds = [ @@ -511,6 +727,11 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, }; const serverResponse: OIDCConfigServerResponse = { name: 'projects/project_id/oauthIdpConfigs/oidc.provider', @@ -518,6 +739,10 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }; const clientRequest: OIDCAuthProviderConfig = { providerId: 'oidc.provider', @@ -525,6 +750,11 @@ describe('OIDCConfig', () => { issuer: 'https://oidc.com/issuer', displayName: 'oidcProviderName', enabled: true, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + }, }; const config = new OIDCConfig(serverResponse); @@ -553,6 +783,21 @@ describe('OIDCConfig', () => { expect(config.enabled).to.be.true; }); + it('should set readonly property clientSecret', () => { + expect(config.clientSecret).to.equal('CLIENT_SECRET'); + }); + + it('should set readonly property expected responseType', () => { + expect(config.responseType).to.deep.equal({ code: true }); + }); + + it('should not throw on no responseType and clientSecret', () => { + const testResponse = deepCopy(serverResponse); + delete testResponse.clientSecret; + delete testResponse.responseType; + expect(() => new OIDCConfig(testResponse)).not.to.throw(); + }); + it('should throw on missing issuer', () => { const invalidResponse = deepCopy(serverResponse); delete invalidResponse.issuer; @@ -615,6 +860,10 @@ describe('OIDCConfig', () => { providerId: 'oidc.provider', issuer: 'https://oidc.com/issuer', clientId: 'CLIENT_ID', + clientSecret: 'CLIENT_SECRET', + responseType: { + code: true, + }, }); }); }); @@ -628,12 +877,22 @@ describe('OIDCConfig', () => { const updateRequest: OIDCUpdateAuthProviderRequest = { clientId: 'CLIENT_ID', displayName: 'OIDC_PROVIDER_DISPLAY_NAME', + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + } }; const updateServerRequest: OIDCConfigServerRequest = { clientId: 'CLIENT_ID', displayName: 'OIDC_PROVIDER_DISPLAY_NAME', issuer: undefined, enabled: undefined, + clientSecret: 'CLIENT_SECRET', + responseType: { + idToken: false, + code: true, + } }; expect(OIDCConfig.buildServerRequest(updateRequest, true)).to.deep.equal(updateServerRequest); }); @@ -645,7 +904,7 @@ describe('OIDCConfig', () => { .to.throw('"OIDCAuthProviderConfig.providerId" must be a valid non-empty string prefixed with "oidc.".'); }); - const nonAuthConfigOptions = [null, undefined, {}, {other: 'value'}]; + const nonAuthConfigOptions = [null, undefined, {}, { other: 'value' }]; nonAuthConfigOptions.forEach((nonAuthConfig) => { it('should return null when no AuthConfig is provided: ' + JSON.stringify(nonAuthConfig), () => { expect(OIDCConfig.buildServerRequest(nonAuthConfig as any)).to.be.null; @@ -676,6 +935,62 @@ describe('OIDCConfig', () => { expect(() => OIDCConfig.validate(partialRequest, true)).not.to.throw(); }); + it('should throw on OAuth responseType contains invalid parameters', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.unknownField = true; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('"unknownField" is not a valid OAuthResponseType parameter.'); + }); + + it('should not throw when exactly one OAuth responseType is true', () => { + const validRequest = deepCopy(clientRequest) as any; + validRequest.responseType.code = false; + validRequest.responseType.idToken = true; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should not throw when only idToken responseType is set to true', () => { + const validRequest = deepCopy(clientRequest) as any; + validRequest.responseType = { idToken: true }; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should not throw when only code responseType is set to true', () => { + const validRequest = deepCopy(clientRequest) as any; + const validResponseType = { code: true }; + validRequest.responseType = validResponseType; + expect(() => OIDCConfig.validate(validRequest, true)).not.to.throw(); + }); + + it('should throw on two OAuth responseTypes set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.idToken = true; + invalidRequest.responseType.code = true; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('Only exactly one OAuth responseType should be set to true.'); + }); + + it('should throw on no OAuth responseType set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + invalidRequest.responseType.idToken = false; + invalidRequest.responseType.code = false; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('Only exactly one OAuth responseType should be set to true.'); + }); + + it('should not throw when responseType is empty', () => { + const testRequest = deepCopy(clientRequest) as any; + testRequest.responseType = {}; + expect(() => OIDCConfig.validate(testRequest, true)).not.to.throw(); + }); + + it('should throw on no client secret when OAuth responseType code flow set to true', () => { + const invalidRequest = deepCopy(clientRequest) as any; + delete invalidRequest.clientSecret; + expect(() => OIDCConfig.validate(invalidRequest, true)) + .to.throw('The OAuth configuration client secret is required to enable OIDC code flow.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on non-null OIDCAuthProviderConfig object:' + JSON.stringify(request), () => { @@ -688,7 +1003,7 @@ describe('OIDCConfig', () => { const invalidClientRequest = deepCopy(clientRequest) as any; invalidClientRequest.unsupported = 'value'; expect(() => OIDCConfig.validate(invalidClientRequest)) - .to.throw(`"unsupported" is not a valid OIDC config parameter.`); + .to.throw('"unsupported" is not a valid OIDC config parameter.'); }); const invalidProviderIds = [ @@ -741,5 +1056,35 @@ describe('OIDCConfig', () => { .to.throw('"OIDCAuthProviderConfig.displayName" must be a valid string.'); }); }); + + const invalidClientSecrets = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidClientSecrets.forEach((invalidClientSecret) => { + it('should throw on invalid clientSecret:' + JSON.stringify(invalidClientSecret), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.clientSecret = invalidClientSecret; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.clientSecret" must be a valid string.'); + }); + }); + + const invalidOAuthResponseIdTokenBooleans = [null, NaN, 0, 1, 'invalid', '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidOAuthResponseIdTokenBooleans.forEach((invalidOAuthResponseIdTokenBoolean) => { + it('should throw on invalid responseType.idToken:' + JSON.stringify(invalidOAuthResponseIdTokenBoolean), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.responseType.idToken = invalidOAuthResponseIdTokenBoolean; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.responseType.idToken" must be a boolean.'); + }); + }); + + const invalidOAuthResponseCodeBooleans = [null, NaN, 0, 1, 'invalid', '', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidOAuthResponseCodeBooleans.forEach((invalidOAuthResponseCodeBoolean) => { + it('should throw on invalid responseType.code:' + JSON.stringify(invalidOAuthResponseCodeBoolean), () => { + const invalidClientRequest = deepCopy(clientRequest) as any; + invalidClientRequest.responseType.code = invalidOAuthResponseCodeBoolean; + expect(() => OIDCConfig.validate(invalidClientRequest)) + .to.throw('"OIDCAuthProviderConfig.responseType.code" must be a boolean.'); + }); + }); }); }); diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts old mode 100755 new mode 100644 index b869580c1f..71957bc4b4 --- 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"); @@ -26,24 +27,28 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {Auth, TenantAwareAuth, BaseAuth, DecodedIdToken} from '../../../src/auth/auth'; -import {UserRecord, UpdateRequest} from '../../../src/auth/user-record'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { Auth, TenantAwareAuth, BaseAuth } from '../../../src/auth/auth'; +import { UserRecord } from '../../../src/auth/user-record'; +import { FirebaseApp } from '../../../src/firebase-app'; import { AuthRequestHandler, TenantAwareAuthRequestHandler, AbstractAuthRequestHandler, } from '../../../src/auth/auth-api-request'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; import * as validator from '../../../src/utils/validator'; import { FirebaseTokenVerifier } from '../../../src/auth/token-verifier'; import { - AuthProviderConfigFilter, OIDCConfig, SAMLConfig, - OIDCConfigServerResponse, SAMLConfigServerResponse, + OIDCConfig, SAMLConfig, OIDCConfigServerResponse, SAMLConfigServerResponse, } from '../../../src/auth/auth-config'; -import {deepCopy} from '../../../src/utils/deep-copy'; +import { deepCopy } from '../../../src/utils/deep-copy'; import { TenantManager } from '../../../src/auth/tenant-manager'; -import { ServiceAccountCredential } from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; +import { auth } from '../../../src/auth/index'; + +import DecodedIdToken = auth.DecodedIdToken; +import UpdateRequest = auth.UpdateRequest; +import AuthProviderConfigFilter = auth.AuthProviderConfigFilter; chai.should(); chai.use(sinonChai); @@ -161,6 +166,7 @@ function getDecodedIdToken(uid: string, authTime: Date, tenantId?: string): Deco sign_in_provider: 'custom', // eslint-disable-line @typescript-eslint/camelcase tenant: tenantId, }, + uid, }; } @@ -186,6 +192,7 @@ function getDecodedSessionCookie(uid: string, authTime: Date, tenantId?: string) sign_in_provider: 'custom', // eslint-disable-line @typescript-eslint/camelcase tenant: tenantId, }, + uid, }; } @@ -220,8 +227,8 @@ function getSAMLConfigServerResponse(providerId: string): SAMLConfigServerRespon ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -234,6 +241,8 @@ function getSAMLConfigServerResponse(providerId: string): SAMLConfigServerRespon } +const INVALID_PROVIDER_IDS = [ + undefined, null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; const TENANT_ID = 'tenantId'; const AUTH_CONFIGS: AuthTest[] = [ { @@ -353,7 +362,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('createCustomToken()', () => { it('should return a jwt', async () => { const token = await auth.createCustomToken('uid1'); - const decodedToken = jwt.decode(token, {complete: true}); + const decodedToken = jwt.decode(token, { complete: true }); expect(decodedToken).to.have.property('header').that.has.property('typ', 'JWT'); }); @@ -378,14 +387,16 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be fulfilled given an app which returns null access tokens', () => { - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves(null); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves(null as any); // createCustomToken() does not rely on an access token and therefore works in this scenario. return auth.createCustomToken(mocks.uid, mocks.developerClaims) .should.eventually.be.fulfilled; }); it('should be fulfilled given an app which returns invalid access tokens', () => { - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves('malformed'); + getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves('malformed' as any); // createCustomToken() does not rely on an access token and therefore works in this scenario. return auth.createCustomToken(mocks.uid, mocks.developerClaims) .should.eventually.be.fulfilled; @@ -637,7 +648,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -654,7 +665,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); } @@ -881,7 +892,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); @@ -898,7 +909,7 @@ AUTH_CONFIGS.forEach((testConfig) => { throw new Error('Unexpected success'); }, (error) => { // Confirm expected error returned. - expect(error).to.deep.equal(expectedError); + expect(error).to.deep.include(expectedError); }); }); } @@ -1142,6 +1153,120 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); + describe('getUserByProviderUid()', () => { + const providerId = 'google.com'; + const providerUid = 'google_uid'; + const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; + const expectedGetAccountInfoResult = getValidGetAccountInfoResponse(tenantId); + const expectedUserRecord = getValidUserRecord(expectedGetAccountInfoResult); + const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + beforeEach(() => sinon.spy(validator, 'isEmail')); + afterEach(() => { + (validator.isEmail as any).restore(); + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no provider id', () => { + expect(() => (auth as any).getUserByProviderUid()) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an invalid provider id', () => { + expect(() => auth.getUserByProviderUid('', 'uid')) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an invalid provider uid', () => { + expect(() => auth.getUserByProviderUid('id', '')) + .to.throw(FirebaseAuthError) + .with.property('code', 'auth/invalid-provider-id'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenAuth.getUserByProviderUid(providerId, providerUid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenAuth.getUserByProviderUid(providerId, providerUid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenAuth.getUserByProviderUid(providerId, providerUid) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve with a UserRecord on success', () => { + // Stub getAccountInfoByEmail to return expected result. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByFederatedUid') + .resolves(expectedGetAccountInfoResult); + stubs.push(stub); + return auth.getUserByProviderUid(providerId, providerUid) + .then((userRecord) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId, providerUid); + // Confirm expected user record response returned. + expect(userRecord).to.deep.equal(expectedUserRecord); + }); + }); + + describe('non-federated providers', () => { + let invokeRequestHandlerStub: sinon.SinonStub; + beforeEach(() => { + invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + + }); + afterEach(() => { + invokeRequestHandlerStub.restore(); + }); + + it('phone lookups should use phoneNumber field', async () => { + await auth.getUserByProviderUid('phone', '+15555550001'); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + phoneNumber: ['+15555550001'], + }); + }); + + it('email lookups should use email field', async () => { + await auth.getUserByProviderUid('email', 'user@example.com'); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + email: ['user@example.com'], + }); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub getAccountInfoByFederatedUid to throw a backend error. + const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByFederatedUid') + .rejects(expectedError); + stubs.push(stub); + return auth.getUserByProviderUid(providerId, providerUid) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(providerId, providerUid); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); + describe('getUsers()', () => { let stubs: sinon.SinonStub[] = []; @@ -1171,7 +1296,7 @@ AUTH_CONFIGS.forEach((testConfig) => { const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') .resolves({}); stubs.push(stub); - const notFoundIds = [{uid: 'id that doesnt exist'}]; + const notFoundIds = [{ uid: 'id that doesnt exist' }]; return auth.getUsers(notFoundIds) .then((getUsersResult) => { expect(getUsersResult.users).to.deep.equal([]); @@ -1203,7 +1328,7 @@ AUTH_CONFIGS.forEach((testConfig) => { }]; const stub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByIdentifiers') - .resolves({users: mockUsers}); + .resolves({ users: mockUsers }); stubs.push(stub); const users = await auth.getUsers([ @@ -1215,13 +1340,13 @@ AUTH_CONFIGS.forEach((testConfig) => { ]); expect(users.users).to.have.deep.members(mockUsers.map((u) => new UserRecord(u))); - expect(users.notFound).to.have.deep.members([{uid: 'this-user-doesnt-exist'}]); + expect(users.notFound).to.have.deep.members([{ uid: 'this-user-doesnt-exist' }]); }); }); describe('deleteUser()', () => { const uid = 'abcdefghijklmnopqrstuvwxyz'; - const expectedDeleteAccountResult = {kind: 'identitytoolkit#DeleteAccountResponse'}; + const expectedDeleteAccountResult = { kind: 'identitytoolkit#DeleteAccountResponse' }; const expectedError = new FirebaseAuthError(AuthClientErrorCode.USER_NOT_FOUND); // Stubs used to simulate underlying api calls. @@ -1505,6 +1630,10 @@ AUTH_CONFIGS.forEach((testConfig) => { emailVerified: expectedUserRecord.emailVerified, password: 'password', phoneNumber: expectedUserRecord.phoneNumber, + providerToLink: { + providerId: 'google.com', + uid: 'google_uid', + }, }; // Stubs used to simulate underlying api calls. let stubs: sinon.SinonStub[] = []; @@ -1548,10 +1677,195 @@ AUTH_CONFIGS.forEach((testConfig) => { }) .catch((error) => { expect(error).to.have.property('code', 'auth/argument-error'); - expect(validator.isNonNullObject).to.have.been.calledOnce.and.calledWith(null); + expect(validator.isNonNullObject).to.have.been.calledWith(null); }); }); + const invalidUpdateRequests: UpdateRequest[] = [ + { providerToLink: { uid: 'google_uid' } }, + { providerToLink: { providerId: 'google.com' } }, + { providerToLink: { providerId: 'google.com', uid: '' } }, + { providerToLink: { providerId: '', uid: 'google_uid' } }, + ]; + invalidUpdateRequests.forEach((invalidUpdateRequest) => { + it('should be rejected given an UpdateRequest with an invalid providerToLink parameter', () => { + expect(() => { + auth.updateUser(uid, invalidUpdateRequest); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + it('should rename providerToLink property to linkProviderUserInfo', async () => { + const invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + localId: uid, + }); + + // Stub getAccountInfoByUid to return a valid result (unchecked; we + // just need it to be valid so as to not crash.) + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + + stubs.push(invokeRequestHandlerStub); + stubs.push(getUserStub); + + await auth.updateUser(uid, { + providerToLink: { + providerId: 'google.com', + uid: 'google_uid', + }, + }); + + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + linkProviderUserInfo: { + providerId: 'google.com', + rawId: 'google_uid', + }, + }); + }); + + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it('should be rejected given a deleteProvider list with an invalid provider ID ' + + JSON.stringify(invalidProviderId), () => { + expect(() => { + auth.updateUser(uid, { + providersToUnlink: [ invalidProviderId as any ], + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + }); + + it('should merge deletion of phone provider with the providersToUnlink list', async () => { + const invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + localId: uid, + }); + + // Stub getAccountInfoByUid to return a valid result (unchecked; we + // just need it to be valid so as to not crash.) + const getUserStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves(expectedGetAccountInfoResult); + + stubs.push(invokeRequestHandlerStub); + stubs.push(getUserStub); + + await auth.updateUser(uid, { + phoneNumber: null, + providersToUnlink: [ 'google.com' ], + }); + + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + deleteProvider: [ 'phone', 'google.com' ], + }); + }); + + describe('non-federated providers', () => { + let invokeRequestHandlerStub: sinon.SinonStub; + let getAccountInfoByUidStub: sinon.SinonStub; + beforeEach(() => { + invokeRequestHandlerStub = sinon.stub(testConfig.RequestHandler.prototype, 'invokeRequestHandler') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + + getAccountInfoByUidStub = sinon.stub(testConfig.RequestHandler.prototype, 'getAccountInfoByUid') + .resolves({ + // nothing here is checked; we just need enough to not crash. + users: [{ + localId: 1, + }], + }); + }); + afterEach(() => { + invokeRequestHandlerStub.restore(); + getAccountInfoByUidStub.restore(); + }); + + it('specifying both email and providerId=email should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + email: 'user@example.com', + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('specifying both phoneNumber and providerId=phone should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + phoneNumber: '+15555550001', + providerToLink: { + providerId: 'phone', + uid: '+15555550001', + }, + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('email linking should use email field', async () => { + await auth.updateUser(uid, { + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + email: 'user@example.com', + }); + }); + + it('phone linking should use phoneNumber field', async () => { + await auth.updateUser(uid, { + providerToLink: { + providerId: 'phone', + uid: '+15555550001', + }, + }); + expect(invokeRequestHandlerStub).to.have.been.calledOnce.and.calledWith( + sinon.match.any, sinon.match.any, { + localId: uid, + phoneNumber: '+15555550001', + }); + }); + + it('specifying both phoneNumber=null and providersToUnlink=phone should be rejected', () => { + expect(() => { + auth.updateUser(uid, { + phoneNumber: null, + providersToUnlink: ['phone'], + }); + }).to.throw(FirebaseAuthError).with.property('code', 'auth/argument-error'); + }); + + it('doesnt mutate the properties parameter', async () => { + const properties: UpdateRequest = { + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }; + await auth.updateUser(uid, properties); + expect(properties).to.deep.equal({ + providerToLink: { + providerId: 'email', + uid: 'user@example.com', + }, + }); + }); + }); + it('should be rejected given an app which returns null access tokens', () => { return nullAccessTokenAuth.updateUser(uid, propertiesToEdit) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); @@ -1732,17 +2046,17 @@ AUTH_CONFIGS.forEach((testConfig) => { const maxResult = 500; const downloadAccountResponse: any = { users: [ - {localId: 'UID1'}, - {localId: 'UID2'}, - {localId: 'UID3'}, + { localId: 'UID1' }, + { localId: 'UID2' }, + { localId: 'UID3' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }; const expectedResult: any = { users: [ - new UserRecord({localId: 'UID1'}), - new UserRecord({localId: 'UID2'}), - new UserRecord({localId: 'UID3'}), + new UserRecord({ localId: 'UID1' }), + new UserRecord({ localId: 'UID2' }), + new UserRecord({ localId: 'UID3' }), ], pageToken: 'NEXT_PAGE_TOKEN', }; @@ -1949,8 +2263,8 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('importUsers()', () => { const users = [ - {uid: '1234', email: 'user@example.com', passwordHash: Buffer.from('password')}, - {uid: '5678', phoneNumber: 'invalid'}, + { uid: '1234', email: 'user@example.com', passwordHash: Buffer.from('password') }, + { uid: '5678', phoneNumber: 'invalid' }, ]; const options = { hash: { @@ -2034,7 +2348,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .throws(expectedOptionsError); stubs.push(uploadAccountStub); expect(() => { - return auth.importUsers(users, {hash: {algorithm: 'invalid' as any}}); + return auth.importUsers(users, { hash: { algorithm: 'invalid' as any } }); }).to.throw(expectedOptionsError); }); @@ -2072,7 +2386,7 @@ AUTH_CONFIGS.forEach((testConfig) => { describe('createSessionCookie()', () => { const tenantId = testConfig.supportsTenantManagement ? undefined : TENANT_ID; const idToken = 'ID_TOKEN'; - const options = {expiresIn: 60 * 60 * 24 * 1000}; + const options = { expiresIn: 60 * 60 * 24 * 1000 }; const sessionCookie = 'SESSION_COOKIE'; const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_ID_TOKEN); const expectedUserRecord = getValidUserRecord(getValidGetAccountInfoResponse(tenantId)); @@ -2138,7 +2452,7 @@ AUTH_CONFIGS.forEach((testConfig) => { stubs.push(sinon.stub(testConfig.Auth.prototype, 'verifyIdToken') .returns(Promise.resolve(decodedIdToken))); // 1 minute duration. - const invalidOptions = {expiresIn: 60 * 1000}; + const invalidOptions = { expiresIn: 60 * 1000 }; return auth.createSessionCookie(idToken, invalidOptions) .should.eventually.be.rejected.and.have.property( 'code', 'auth/invalid-session-cookie-duration'); @@ -2234,9 +2548,9 @@ AUTH_CONFIGS.forEach((testConfig) => { }); const emailActionFlows: EmailActionTest[] = [ - {api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false}, - {api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false}, - {api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true}, + { api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false }, + { api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false }, + { api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true }, ]; emailActionFlows.forEach((emailActionFlow) => { describe(`${emailActionFlow.api}()`, () => { @@ -2364,9 +2678,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - const invalidProviderIds = [ - undefined, null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; - invalidProviderIds.forEach((invalidProviderId) => { + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { return (auth as Auth).getProviderConfig(invalidProviderId as any) .then(() => { @@ -2448,8 +2760,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -2584,7 +2896,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .stub(testConfig.RequestHandler.prototype, 'listOAuthIdpConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); - return (auth as Auth).listProviderConfigs({type: 'oidc'}) + return (auth as Auth).listProviderConfigs({ type: 'oidc' }) .then((response) => { expect(response).to.deep.equal(expectedResult); // Confirm underlying API called with expected parameters. @@ -2679,7 +2991,7 @@ AUTH_CONFIGS.forEach((testConfig) => { .stub(testConfig.RequestHandler.prototype, 'listInboundSamlConfigs') .resolves(listConfigsResponse); stubs.push(listConfigsStub); - return (auth as Auth).listProviderConfigs({type: 'saml'}) + return (auth as Auth).listProviderConfigs({ type: 'saml' }) .then((response) => { expect(response).to.deep.equal(expectedResult); // Confirm underlying API called with expected parameters. @@ -2737,15 +3049,16 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - it('should be rejected given an invalid provider ID', () => { - const invalidProviderId = ''; - return (auth as Auth).deleteProviderConfig(invalidProviderId) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-provider-id'); - }); + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + return (auth as Auth).deleteProviderConfig(invalidProviderId as any) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-provider-id'); + }); + }); }); it('should be rejected given an app which returns null access tokens', () => { @@ -2856,15 +3169,16 @@ AUTH_CONFIGS.forEach((testConfig) => { .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-provider-id'); }); - it('should be rejected given an invalid provider ID', () => { - const invalidProviderId = ''; - return (auth as Auth).updateProviderConfig(invalidProviderId, oidcConfigOptions) - .then(() => { - throw new Error('Unexpected success'); - }) - .catch((error) => { - expect(error).to.have.property('code', 'auth/invalid-provider-id'); - }); + INVALID_PROVIDER_IDS.forEach((invalidProviderId) => { + it(`should be rejected given an invalid provider ID "${JSON.stringify(invalidProviderId)}"`, () => { + return (auth as Auth).updateProviderConfig(invalidProviderId as any, oidcConfigOptions) + .then(() => { + throw new Error('Unexpected success'); + }) + .catch((error) => { + expect(error).to.have.property('code', 'auth/invalid-provider-id'); + }); + }); }); it('should be rejected given no options', () => { @@ -2966,8 +3280,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -3133,8 +3447,8 @@ AUTH_CONFIGS.forEach((testConfig) => { ssoUrl: 'https://example.com/login', signRequest: true, idpCertificates: [ - {x509Certificate: 'CERT1'}, - {x509Certificate: 'CERT2'}, + { x509Certificate: 'CERT1' }, + { x509Certificate: 'CERT2' }, ], }, spConfig: { @@ -3181,12 +3495,112 @@ AUTH_CONFIGS.forEach((testConfig) => { }); }); - if (testConfig.Auth === Auth) { - describe('INTERNAL.delete()', () => { - it('should delete Auth instance', () => { - (auth as Auth).INTERNAL.delete().should.eventually.be.fulfilled; + describe('auth emulator support', () => { + + let mockAuth = testConfig.init(mocks.app()); + 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, '', { 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, + }, { + iat: oneSecBeforeValidSince, + auth_time: oneSecBeforeValidSince, // eslint-disable-line @typescript-eslint/camelcase + }); + + // 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, // eslint-disable-line @typescript-eslint/camelcase + }); + + // 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' }); + + 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/tenant-manager.spec.ts b/test/unit/auth/tenant-manager.spec.ts index 82f89599d9..52bf955ccd 100644 --- a/test/unit/auth/tenant-manager.spec.ts +++ b/test/unit/auth/tenant-manager.spec.ts @@ -23,11 +23,16 @@ import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {AuthRequestHandler} from '../../../src/auth/auth-api-request'; -import {Tenant, TenantOptions, TenantServerResponse, ListTenantsResult} from '../../../src/auth/tenant'; -import {TenantManager} from '../../../src/auth/tenant-manager'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { AuthRequestHandler } from '../../../src/auth/auth-api-request'; +import { Tenant, TenantServerResponse } from '../../../src/auth/tenant'; +import { TenantManager } from '../../../src/auth/tenant-manager'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { auth } from '../../../src/auth/index'; + +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; +import ListTenantsResult = auth.ListTenantsResult; chai.should(); chai.use(sinonChai); @@ -47,6 +52,7 @@ describe('TenantManager', () => { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: false, + enableAnonymousUser: true, }; before(() => { @@ -175,15 +181,15 @@ describe('TenantManager', () => { const maxResult = 500; const listTenantsResponse: any = { tenants : [ - {name: 'projects/project-id/tenants/tenant-id1'}, - {name: 'projects/project-id/tenants/tenant-id2'}, + { name: 'projects/project-id/tenants/tenant-id1' }, + { name: 'projects/project-id/tenants/tenant-id2' }, ], nextPageToken: 'NEXT_PAGE_TOKEN', }; const expectedResult: ListTenantsResult = { tenants: [ - new Tenant({name: 'projects/project-id/tenants/tenant-id1'}), - new Tenant({name: 'projects/project-id/tenants/tenant-id2'}), + new Tenant({ name: 'projects/project-id/tenants/tenant-id1' }), + new Tenant({ name: 'projects/project-id/tenants/tenant-id2' }), ], pageToken: 'NEXT_PAGE_TOKEN', }; @@ -377,12 +383,13 @@ describe('TenantManager', () => { }); describe('createTenant()', () => { - const tenantOptions: TenantOptions = { + 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( @@ -412,7 +419,7 @@ describe('TenantManager', () => { it('should be rejected given TenantOptions with invalid type property', () => { // Create tenant using invalid type. This should throw an argument error. - return tenantManager.createTenant({type: 'invalid'} as any) + return tenantManager.createTenant({ type: 'invalid' } as any) .then(() => { throw new Error('Unexpected success'); }) @@ -469,12 +476,13 @@ describe('TenantManager', () => { describe('updateTenant()', () => { const tenantId = 'tenant-id'; - const tenantOptions: TenantOptions = { + 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( @@ -509,7 +517,7 @@ describe('TenantManager', () => { }); it('should be rejected given invalid TenantOptions', () => { - return tenantManager.updateTenant(tenantId, null as unknown as TenantOptions) + return tenantManager.updateTenant(tenantId, null as unknown as UpdateTenantRequest) .then(() => { throw new Error('Unexpected success'); }) @@ -521,7 +529,7 @@ describe('TenantManager', () => { it('should be rejected given TenantOptions with invalid update property', () => { // Updating the tenantId of an existing tenant will throw an error as tenantId is // an immutable property. - return tenantManager.updateTenant(tenantId, {tenantId: 'unmodifiable'} as any) + return tenantManager.updateTenant(tenantId, { tenantId: 'unmodifiable' } as any) .then(() => { throw new Error('Unexpected success'); }) diff --git a/test/unit/auth/tenant.spec.ts b/test/unit/auth/tenant.spec.ts old mode 100755 new mode 100644 index 1fb1e24dfa..b2ebe6a5d1 --- a/test/unit/auth/tenant.spec.ts +++ b/test/unit/auth/tenant.spec.ts @@ -19,12 +19,14 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {deepCopy} from '../../../src/utils/deep-copy'; -import {EmailSignInConfig, EmailSignInProviderConfig} from '../../../src/auth/auth-config'; -import { - Tenant, TenantOptions, TenantServerResponse, -} from '../../../src/auth/tenant'; +import { deepCopy } from '../../../src/utils/deep-copy'; +import { EmailSignInConfig, MultiFactorAuthConfig } from '../../../src/auth/auth-config'; +import { Tenant, TenantServerResponse } from '../../../src/auth/tenant'; +import { auth } from '../../../src/auth/index'; +import EmailSignInProviderConfig = auth.EmailSignInProviderConfig; +import CreateTenantRequest = auth.CreateTenantRequest; +import UpdateTenantRequest = auth.UpdateTenantRequest; chai.should(); chai.use(sinonChai); @@ -38,9 +40,40 @@ describe('Tenant', () => { displayName: 'TENANT-DISPLAY-NAME', allowPasswordSignup: true, enableEmailLinkSignin: true, + mfaConfig: { + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + }; + + const clientRequest: UpdateTenantRequest = { + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + multiFactorConfig: { + state: 'ENABLED', + factorIds: ['phone'], + }, + testPhoneNumbers: { + '+16505551234': '019287', + '+16505550676': '985235', + }, + }; + + const serverRequestWithoutMfa: TenantServerResponse = { + name: 'projects/project1/tenants/TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + allowPasswordSignup: true, + enableEmailLinkSignin: true, }; - const clientRequest: TenantOptions = { + const clientRequestWithoutMfa: UpdateTenantRequest = { displayName: 'TENANT-DISPLAY-NAME', emailSignInConfig: { enabled: true, @@ -52,7 +85,15 @@ describe('Tenant', () => { const createRequest = true; describe('for an update request', () => { - it('should return the expected server request', () => { + it('should return the expected server request without multi-factor and phone config', () => { + const tenantOptionsClientRequest = deepCopy(clientRequestWithoutMfa); + const tenantOptionsServerRequest = deepCopy(serverRequestWithoutMfa); + delete tenantOptionsServerRequest.name; + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + + it('should return the expected server request with multi-factor and phone config', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); const tenantOptionsServerRequest = deepCopy(serverRequest); delete tenantOptionsServerRequest.name; @@ -75,6 +116,33 @@ describe('Tenant', () => { }).to.throw('"EmailSignInConfig.enabled" must be a boolean.'); }); + it('should throw on invalid MultiFactorConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.multiFactorConfig.state = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"MultiFactorConfig.state" must be either "ENABLED" or "DISABLED".'); + }); + + it('should throw on invalid testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.testPhoneNumbers = 'invalid'; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); + }).to.throw('"testPhoneNumbers" must be a map of phone number / code pairs.'); + }); + + it('should not throw on null testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + tenantOptionsClientRequest.testPhoneNumbers = null; + delete tenantOptionsServerRequest.name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest)) + .to.deep.equal(tenantOptionsServerRequest); + }); + it('should not throw on valid client request object', () => { const tenantOptionsClientRequest = deepCopy(clientRequest); expect(() => { @@ -96,7 +164,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.unsupported = 'value'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, !createRequest); - }).to.throw(`"unsupported" is not a valid UpdateTenantRequest parameter.`); + }).to.throw('"unsupported" is not a valid UpdateTenantRequest parameter.'); }); const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -112,8 +180,17 @@ describe('Tenant', () => { }); describe('for a create request', () => { - it('should return the expected server request', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + 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.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.name; @@ -122,13 +199,41 @@ describe('Tenant', () => { }); it('should throw on invalid EmailSignInConfig', () => { - const tenantOptionsClientRequest: TenantOptions = deepCopy(clientRequest); + const tenantOptionsClientRequest: CreateTenantRequest = deepCopy(clientRequest); tenantOptionsClientRequest.emailSignInConfig = null as unknown as EmailSignInProviderConfig; expect(() => Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest)) .to.throw('"EmailSignInConfig" must be a non-null object.'); }); + it('should throw on invalid MultiFactorConfig attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.multiFactorConfig.factorIds = ['invalid']; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalid" is not a valid "AuthFactorType".',); + }); + + it('should throw on invalid testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest) as any; + tenantOptionsClientRequest.testPhoneNumbers = { 'invalid': '123456' }; + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"invalid" is not a valid E.164 standard compliant phone number.'); + }); + + it('should throw on null testPhoneNumbers attribute', () => { + const tenantOptionsClientRequest = deepCopy(clientRequest); + const tenantOptionsServerRequest = deepCopy(serverRequest); + tenantOptionsClientRequest.testPhoneNumbers = null; + delete tenantOptionsServerRequest.name; + tenantOptionsServerRequest.testPhoneNumbers = {}; + + expect(() => { + Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); + }).to.throw('"CreateTenantRequest.testPhoneNumbers" must be a non-null object.'); + }); + const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((request) => { it('should throw on invalid CreateTenantRequest:' + JSON.stringify(request), () => { @@ -143,7 +248,7 @@ describe('Tenant', () => { tenantOptionsClientRequest.unsupported = 'value'; expect(() => { Tenant.buildServerRequest(tenantOptionsClientRequest, createRequest); - }).to.throw(`"unsupported" is not a valid CreateTenantRequest parameter.`); + }).to.throw('"unsupported" is not a valid CreateTenantRequest parameter.'); }); const invalidTenantNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; @@ -198,6 +303,19 @@ describe('Tenant', () => { expect(tenant.emailSignInConfig).to.deep.equal(expectedEmailSignInConfig); }); + it('should set readonly property multiFactorConfig', () => { + const expectedMultiFactorConfig = new MultiFactorAuthConfig({ + state: 'ENABLED', + enabledProviders: ['PHONE_SMS'], + }); + expect(tenant.multiFactorConfig).to.deep.equal(expectedMultiFactorConfig); + }); + + it('should set readonly property testPhoneNumbers', () => { + expect(tenant.testPhoneNumbers).to.deep.equal( + deepCopy(clientRequest.testPhoneNumbers)); + }); + it('should throw when no tenant ID is provided', () => { const invalidOptions = deepCopy(serverRequest); // Use resource name that does not include a tenant ID. @@ -233,6 +351,25 @@ describe('Tenant', () => { enabled: true, passwordRequired: false, }, + anonymousSignInEnabled: false, + multiFactorConfig: deepCopy(clientRequest.multiFactorConfig), + testPhoneNumbers: deepCopy(clientRequest.testPhoneNumbers), + }); + }); + + it('should not populate optional fields if not available', () => { + const serverRequestCopyWithoutMfa: TenantServerResponse = deepCopy(serverRequest); + delete serverRequestCopyWithoutMfa.mfaConfig; + delete serverRequestCopyWithoutMfa.testPhoneNumbers; + + expect(new Tenant(serverRequestCopyWithoutMfa).toJSON()).to.deep.equal({ + tenantId: 'TENANT-ID', + displayName: 'TENANT-DISPLAY-NAME', + emailSignInConfig: { + enabled: true, + passwordRequired: false, + }, + anonymousSignInEnabled: false, }); }); }); diff --git a/test/unit/auth/token-generator.spec.ts b/test/unit/auth/token-generator.spec.ts index a257026193..6a6d148b09 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"); @@ -25,14 +26,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../../resources/mocks'; import { - BLACKLISTED_CLAIMS, FirebaseTokenGenerator, ServiceAccountSigner, IAMSigner, + BLACKLISTED_CLAIMS, FirebaseTokenGenerator, EmulatedSigner, handleCryptoSignerError } from '../../../src/auth/token-generator'; +import { CryptoSignerError, CryptoSignerErrorCode, ServiceAccountSigner } from '../../../src/utils/crypto-signer'; -import { ServiceAccountCredential } from '../../../src/auth/credential'; -import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; -import { FirebaseApp } from '../../../src/firebase-app'; -import * as utils from '../utils'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { FirebaseAuthError } from '../../../src/utils/error'; +import * as utils from '../utils'; chai.should(); chai.use(sinonChai); @@ -65,195 +65,6 @@ function verifyToken(token: string, publicKey: string): Promise { }); } -describe('CryptoSigner', () => { - describe('ServiceAccountSigner', () => { - it('should throw given no arguments', () => { - expect(() => { - const anyServiceAccountSigner: any = ServiceAccountSigner; - return new anyServiceAccountSigner(); - }).to.throw('Must provide a service account credential to initialize ServiceAccountSigner'); - }); - - it('should not throw given a valid certificate', () => { - expect(() => { - return new ServiceAccountSigner(new ServiceAccountCredential(mocks.certificateObject)); - }).not.to.throw(); - }); - - it('should sign using the private_key in the certificate', () => { - const payload = Buffer.from('test'); - const cert = new ServiceAccountCredential(mocks.certificateObject); - - // eslint-disable-next-line @typescript-eslint/no-var-requires - const crypto = require('crypto'); - const rsa = crypto.createSign('RSA-SHA256'); - rsa.update(payload); - const result = rsa.sign(cert.privateKey, 'base64'); - - const signer = new ServiceAccountSigner(cert); - return signer.sign(payload).then((signature) => { - expect(signature.toString('base64')).to.equal(result); - }); - }); - - it('should return the client_email from the certificate', () => { - const cert = new ServiceAccountCredential(mocks.certificateObject); - const signer = new ServiceAccountSigner(cert); - return signer.getAccountId().should.eventually.equal(cert.clientEmail); - }); - }); - - describe('IAMSigner', () => { - let mockApp: FirebaseApp; - let getTokenStub: sinon.SinonStub; - const mockAccessToken: string = utils.generateRandomAccessToken(); - - beforeEach(() => { - mockApp = mocks.app(); - getTokenStub = utils.stubGetAccessToken(mockAccessToken, mockApp); - return mockApp.INTERNAL.getToken(); - }); - - afterEach(() => { - getTokenStub.restore(); - return mockApp.delete(); - }); - - it('should throw given no arguments', () => { - expect(() => { - const anyIAMSigner: any = IAMSigner; - return new anyIAMSigner(); - }).to.throw('Must provide a HTTP client to initialize IAMSigner'); - }); - - describe('explicit service account ID', () => { - const response = {signedBlob: Buffer.from('testsignature').toString('base64')}; - const input = Buffer.from('input'); - const signRequest = { - method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob`, - headers: {Authorization: `Bearer ${mockAccessToken}`}, - data: {payload: input.toString('base64')}, - }; - let stub: sinon.SinonStub; - - afterEach(() => { - stub.restore(); - }); - - it('should sign using the IAM service', () => { - const expectedResult = utils.responseFrom(response); - stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler, 'test-service-account'); - return signer.sign(input).then((signature) => { - expect(signature.toString('base64')).to.equal(response.signedBlob); - expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); - }); - }); - - it('should fail if the IAM service responds with an error', () => { - const expectedResult = utils.errorFrom({ - error: { - status: 'PROJECT_NOT_FOUND', - message: 'test reason', - }, - }); - stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler, 'test-service-account'); - return signer.sign(input).catch((err) => { - const message = 'test reason; Please refer to ' + - 'https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on ' + - 'how to use and troubleshoot this feature.'; - expect(err.message).to.equal(message); - expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); - }); - }); - - it('should return the explicitly specified service account', () => { - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp), 'test-service-account'); - return signer.getAccountId().should.eventually.equal('test-service-account'); - }); - }); - - describe('auto discovered service account', () => { - const input = Buffer.from('input'); - const response = {signedBlob: Buffer.from('testsignature').toString('base64')}; - const metadataRequest = { - method: 'GET', - url: `http://metadata/computeMetadata/v1/instance/service-accounts/default/email`, - headers: {'Metadata-Flavor': 'Google'}, - }; - const signRequest = { - method: 'POST', - url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob`, - headers: {Authorization: `Bearer ${mockAccessToken}`}, - data: {payload: input.toString('base64')}, - }; - let stub: sinon.SinonStub; - - afterEach(() => { - stub.restore(); - }); - - it('should sign using the IAM service', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - stub.onCall(1).resolves(utils.responseFrom(response)); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler); - return signer.sign(input).then((signature) => { - expect(signature.toString('base64')).to.equal(response.signedBlob); - expect(stub).to.have.been.calledTwice; - expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); - expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); - }); - }); - - it('should fail if the IAM service responds with an error', () => { - const expectedResult = { - error: { - status: 'PROJECT_NOT_FOUND', - message: 'test reason', - }, - }; - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - stub.onCall(1).rejects(utils.errorFrom(expectedResult)); - const requestHandler = new AuthorizedHttpClient(mockApp); - const signer = new IAMSigner(requestHandler); - return signer.sign(input).catch((err) => { - const message = 'test reason; Please refer to ' + - 'https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on ' + - 'how to use and troubleshoot this feature.'; - expect(err.message).to.equal(message); - expect(stub).to.have.been.calledTwice; - expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); - expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); - }); - }); - - it('should return the discovered service account', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); - return signer.getAccountId().should.eventually.equal('discovered-service-account'); - }); - - it('should return the expected error when failed to contact the Metadata server', () => { - stub = sinon.stub(HttpClient.prototype, 'send'); - stub.onCall(0).rejects(utils.errorFrom('test error')); - const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); - const expected = 'Failed to determine service account. Make sure to initialize the SDK with ' + - 'a service account credential. Alternatively specify a service account with ' + - 'iam.serviceAccounts.signBlob permission.'; - return signer.getAccountId().should.eventually.be.rejectedWith(expected); - }); - }); - }); -}); - describe('FirebaseTokenGenerator', () => { const tenantId = 'tenantId1'; const cert = new ServiceAccountCredential(mocks.certificateObject); @@ -308,6 +119,29 @@ describe('FirebaseTokenGenerator', () => { tokenGenerator: new FirebaseTokenGenerator(new ServiceAccountSigner(cert), tenantId), }]; + describe('Emulator', () => { + const signer = new EmulatedSigner(); + const tokenGenerator = new FirebaseTokenGenerator(signer); + + it('should generate a valid unsigned token', async () => { + const uid = 'uid123'; + const claims = { foo: 'bar' }; + const token = await tokenGenerator.createCustomToken(uid, claims); + + // Check that verify doesn't throw + // Note: the types for jsonwebtoken are wrong so we have to disguise the 'null' + jwt.verify(token, '', { algorithms: ['none'] }); + + // Decode and check all three segments + const { header, payload, signature } = jwt.decode(token, { complete: true }) as { [key: string]: any }; + expect(header).to.deep.equal({ alg: 'none', typ: 'JWT' }); + expect(payload['uid']).to.equal(uid); + expect(payload['claims']).to.deep.equal(claims); + expect(signature).to.equal(''); + }); + + }); + tokenGeneratorConfigs.forEach((tokenGeneratorConfig) => { describe(tokenGeneratorConfig.name, () => { const tokenGenerator = tokenGeneratorConfig.tokenGenerator; @@ -360,7 +194,7 @@ describe('FirebaseTokenGenerator', () => { BLACKLISTED_CLAIMS.forEach((blacklistedClaim) => { it('should throw given a developer claims object with a blacklisted claim: ' + blacklistedClaim, () => { - const blacklistedDeveloperClaims: {[key: string]: any} = _.clone(mocks.developerClaims); + const blacklistedDeveloperClaims: { [key: string]: any } = _.clone(mocks.developerClaims); blacklistedDeveloperClaims[blacklistedClaim] = true; expect(() => { tokenGenerator.createCustomToken(mocks.uid, blacklistedDeveloperClaims); @@ -391,7 +225,7 @@ describe('FirebaseTokenGenerator', () => { return tokenGenerator.createCustomToken(mocks.uid) .then((token) => { const decoded = jwt.decode(token); - const expected: {[key: string]: any} = { + const expected: { [key: string]: any } = { uid: mocks.uid, iat: 1, exp: ONE_HOUR_IN_SECONDS + 1, @@ -416,7 +250,7 @@ describe('FirebaseTokenGenerator', () => { .then((token) => { const decoded = jwt.decode(token); - const expected: {[key: string]: any} = { + const expected: { [key: string]: any } = { uid: mocks.uid, iat: 1, exp: ONE_HOUR_IN_SECONDS + 1, @@ -502,4 +336,47 @@ describe('FirebaseTokenGenerator', () => { }); }); }); + + describe('handleCryptoSignerError', () => { + it('should convert CryptoSignerError to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.INVALID_ARGUMENT, + message: 'test error.', + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/argument-error'); + expect(authError).to.have.property('message', 'test error.'); + }); + + it('should convert CryptoSignerError HttpError to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom({ + error: { + message: 'server error.', + }, + }) + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/internal-error'); + expect(authError).to.have.property('message', 'server error.; Please refer to https://firebase.google.com/docs/auth/admin/create-custom-tokens for more details on how to use and troubleshoot this feature. Raw server response: "{"error":{"message":"server error."}}"'); + }); + + it('should convert CryptoSignerError HttpError with no errorcode to FirebaseAuthError', () => { + const cryptoError = new CryptoSignerError({ + code: CryptoSignerErrorCode.SERVER_ERROR, + message: 'test error.', + cause: utils.errorFrom('server error.') + }); + const authError = handleCryptoSignerError(cryptoError); + expect(authError).to.be.an.instanceof(FirebaseAuthError); + expect(authError).to.have.property('code', 'auth/internal-error'); + expect(authError).to.have.property('message', + 'Error returned from server: null. Additionally, an internal error occurred ' + + 'while attempting to extract the errorcode from the error.'); + }); + }); }); diff --git a/test/unit/auth/token-verifier.spec.ts b/test/unit/auth/token-verifier.spec.ts index bf95f96380..a8afe7167b 100644 --- a/test/unit/auth/token-verifier.spec.ts +++ b/test/unit/auth/token-verifier.spec.ts @@ -16,24 +16,25 @@ 'use strict'; -// Use untyped import syntax for Node built-ins -import https = require('https'); - import * as _ from 'lodash'; import * as chai from 'chai'; import * as nock from 'nock'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; +import { Agent } from 'http'; + import LegacyFirebaseTokenGenerator = require('firebase-token-generator'); import * as mocks from '../../resources/mocks'; -import {FirebaseTokenGenerator, ServiceAccountSigner} from '../../../src/auth/token-generator'; +import { FirebaseTokenGenerator } from '../../../src/auth/token-generator'; +import { ServiceAccountSigner } from '../../../src/utils/crypto-signer'; import * as verifier from '../../../src/auth/token-verifier'; -import {ServiceAccountCredential} from '../../../src/auth/credential'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import { AuthClientErrorCode } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; +import { JwtError, JwtErrorCode, PublicKeySignatureVerifier } from '../../../src/utils/jwt'; chai.should(); chai.use(sinonChai); @@ -42,66 +43,16 @@ chai.use(chaiAsPromised); const expect = chai.expect; const ONE_HOUR_IN_SECONDS = 60 * 60; -const idTokenPublicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; - -/** - * Returns a mocked out success response from the URL containing the public keys for the Google certs. - * - * @param {string=} path URL path to which the mock request should be made. If not specified, defaults - * to the URL path of ID token public key certificates. - * @return {Object} A nock response object. - */ -function mockFetchPublicKeys(path: string = idTokenPublicCertPath): nock.Scope { - const mockedResponse: {[key: string]: string} = {}; - mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public; - return nock('https://www.googleapis.com') - .get(path) - .reply(200, mockedResponse, { - 'cache-control': 'public, max-age=1, must-revalidate, no-transform', - }); -} - -/** - * Returns a mocked out success response from the URL containing the public keys for the Google certs - * which contains a public key which won't match the mocked token. - * - * @return {Object} A nock response object. - */ -function mockFetchWrongPublicKeys(): nock.Scope { - const mockedResponse: {[key: string]: string} = {}; - mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[1].public; - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .reply(200, mockedResponse, { - 'cache-control': 'public, max-age=1, must-revalidate, no-transform', - }); -} - -/** - * Returns a mocked out error response from the URL containing the public keys for the Google certs. - * The status code is 200 but the response itself will contain an 'error' key. - * - * @return {Object} A nock response object. - */ -function mockFetchPublicKeysWithErrorResponse(): nock.Scope { - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .reply(200, { - error: 'message', - error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase - }); -} -/** - * Returns a mocked out failed response from the URL containing the public keys for the Google certs. - * The status code is non-200 and the response itself will fail. - * - * @return {Object} A nock response object. - */ -function mockFailedFetchPublicKeys(): nock.Scope { - return nock('https://www.googleapis.com') - .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') - .replyWithError('message'); +function createTokenVerifier( + app: FirebaseApp +): 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 + ); } @@ -110,20 +61,12 @@ describe('FirebaseTokenVerifier', () => { let tokenVerifier: verifier.FirebaseTokenVerifier; let tokenGenerator: FirebaseTokenGenerator; let clock: sinon.SinonFakeTimers | undefined; - let httpsSpy: sinon.SinonSpy; beforeEach(() => { // Needed to generate custom token for testing. app = mocks.app(); const cert = new ServiceAccountCredential(mocks.certificateObject); tokenGenerator = new FirebaseTokenGenerator(new ServiceAccountSigner(cert)); - tokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - app, - ); - httpsSpy = sinon.spy(https, 'request'); + tokenVerifier = createTokenVerifier(app); }); afterEach(() => { @@ -131,7 +74,6 @@ describe('FirebaseTokenVerifier', () => { clock.restore(); clock = undefined; } - httpsSpy.restore(); }); after(() => { @@ -143,7 +85,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { tokenVerifier = new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -163,7 +104,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( invalidCertUrl as any, - 'RS256', 'https://www.example.com/issuer/', verifier.ID_TOKEN_INFO, app, @@ -172,27 +112,12 @@ describe('FirebaseTokenVerifier', () => { }); }); - const invalidAlgorithms = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, '']; - invalidAlgorithms.forEach((invalidAlgorithm) => { - it('should throw given an invalid algorithm: ' + JSON.stringify(invalidAlgorithm), () => { - expect(() => { - new verifier.FirebaseTokenVerifier( - 'https://www.example.com/publicKeys', - invalidAlgorithm as any, - 'https://www.example.com/issuer/', - verifier.ID_TOKEN_INFO, - app); - }).to.throw('The provided JWT algorithm is an empty string.'); - }); - }); - const invalidIssuers = [null, NaN, 0, 1, true, false, [], {}, { a: 1 }, _.noop, 'file://invalid']; invalidIssuers.forEach((invalidIssuer) => { it('should throw given a non-URL issuer: ' + JSON.stringify(invalidIssuer), () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', invalidIssuer as any, verifier.ID_TOKEN_INFO, app, @@ -207,7 +132,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -228,7 +152,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -249,7 +172,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -270,7 +192,6 @@ describe('FirebaseTokenVerifier', () => { expect(() => { new verifier.FirebaseTokenVerifier( 'https://www.example.com/publicKeys', - 'RS256', 'https://www.example.com/issuer/', { url: 'https://docs.example.com/verify-tokens', @@ -288,10 +209,14 @@ describe('FirebaseTokenVerifier', () => { describe('verifyJWT()', () => { let mockedRequests: nock.Scope[] = []; + let stubs: sinon.SinonStub[] = []; afterEach(() => { _.forEach(mockedRequests, (mockedRequest) => mockedRequest.done()); mockedRequests = []; + + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; }); it('should throw given no Firebase JWT token', () => { @@ -322,7 +247,6 @@ describe('FirebaseTokenVerifier', () => { it('should throw if the token verifier was initialized with no "project_id"', () => { const tokenVerifierWithNoProjectId = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', 'https://securetoken.google.com/', verifier.ID_TOKEN_INFO, mocks.mockCredentialApp(), @@ -336,27 +260,12 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a Firebase JWT token with no kid', () => { const mockIdToken = mocks.generateIdToken({ - header: {foo: 'bar'}, + header: { foo: 'bar' }, }); return tokenVerifier.verifyJWT(mockIdToken) .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim'); }); - it('should be rejected given a Firebase JWT token with a kid which does not match any of the ' + - 'actual public keys', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken({ - header: { - kid: 'wrongkid', - }, - }); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has "kid" claim which does not ' + - 'correspond to a known public key'); - }); - it('should be rejected given a Firebase JWT token with an incorrect algorithm', () => { const mockIdToken = mocks.generateIdToken({ algorithm: 'HS256', @@ -383,8 +292,26 @@ describe('FirebaseTokenVerifier', () => { .should.eventually.be.rejectedWith('Firebase ID token has incorrect "iss" (issuer) claim'); }); + it('should be rejected when the verifier throws no maching kid error', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.NO_MATCHING_KID, 'No matching key ID.')); + stubs.push(verifierStub); + + const mockIdToken = mocks.generateIdToken({ + header: { + kid: 'wrongkid', + }, + }); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has "kid" claim which does not ' + + 'correspond to a known public key'); + }); + it('should be rejected given a Firebase JWT token with a subject with greater than 128 characters', () => { - mockedRequests.push(mockFetchPublicKeys()); + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); // uid of length 128 should be fulfilled let uid = Array(129).join('a'); @@ -405,62 +332,59 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should be rejected given an expired Firebase JWT token', () => { - mockedRequests.push(mockFetchPublicKeys()); - - clock = sinon.useFakeTimers(1000); + it('should be rejected when the verifier throws for expired Firebase JWT token', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); const mockIdToken = mocks.generateIdToken(); - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); - - // Token should still be valid - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - clock!.tick(1); - - // Token should now be invalid - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' + - 'app and try again (auth/id-token-expired)') - .and.have.property('code', 'auth/id-token-expired'); - }); + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has expired. Get a fresh ID token from your client ' + + 'app and try again (auth/id-token-expired)') + .and.have.property('code', 'auth/id-token-expired'); }); - it('should be rejected given an expired Firebase session cookie', () => { + it('should be rejected when the verifier throws for expired Firebase session cookie', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.TOKEN_EXPIRED, 'Expired token.')); + stubs.push(verifierStub); + const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, ); - mockedRequests.push(mockFetchPublicKeys('/identitytoolkit/v3/relyingparty/publicKeys')); - - clock = sinon.useFakeTimers(1000); const mockSessionCookie = mocks.generateSessionCookie(); - clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) + .should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' + + 'your client app and try again (auth/session-cookie-expired).') + .and.have.property('code', 'auth/session-cookie-expired'); + }); - // Cookie should still be valid - return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie).then(() => { - clock!.tick(1); + it('should be rejected when the verifier throws invalid signature for a Firebase JWT token.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.INVALID_SIGNATURE, 'invalid signature.')); + stubs.push(verifierStub); - // Cookie should now be invalid - return tokenVerifierSessionCookie.verifyJWT(mockSessionCookie) - .should.eventually.be.rejectedWith('Firebase session cookie has expired. Get a fresh session cookie from ' + - 'your client app and try again (auth/session-cookie-expired).') - .and.have.property('code', 'auth/session-cookie-expired'); - }); + const mockIdToken = mocks.generateIdToken(); + + return tokenVerifier.verifyJWT(mockIdToken) + .should.eventually.be.rejectedWith('Firebase ID token has invalid signature'); }); - it('should be rejected given a Firebase JWT token which was not signed with the kid it specifies', () => { - mockedRequests.push(mockFetchWrongPublicKeys()); + it('should be rejected when the verifier throws key fetch error.', () => { + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .rejects(new JwtError(JwtErrorCode.KEY_FETCH_ERROR, 'Error fetching public keys.')); + stubs.push(verifierStub); const mockIdToken = mocks.generateIdToken(); return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Firebase ID token has invalid signature'); + .should.eventually.be.rejectedWith('Error fetching public keys.'); }); it('should be rejected given a custom token with error using article "an" before JWT short name', () => { @@ -474,7 +398,6 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a custom token with error using article "a" before JWT short name', () => { const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, @@ -500,7 +423,6 @@ describe('FirebaseTokenVerifier', () => { it('should be rejected given a legacy custom token with error using article "a" before JWT short name', () => { const tokenVerifierSessionCookie = new verifier.FirebaseTokenVerifier( 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/publicKeys', - 'RS256', 'https://session.firebase.google.com/', verifier.SESSION_COOKIE_INFO, app, @@ -515,8 +437,26 @@ describe('FirebaseTokenVerifier', () => { 'verifySessionCookie() expects a session cookie, but was given a legacy custom token'); }); + it('AppOptions.httpAgent should be passed to the verifier', () => { + const mockAppWithAgent = mocks.appWithOptions({ + httpAgent: new Agent() + }); + const agentForApp = mockAppWithAgent.options.httpAgent; + const verifierSpy = sinon.spy(PublicKeySignatureVerifier, 'withCertificateUrl'); + + expect(verifierSpy.args).to.be.empty; + + createTokenVerifier(mockAppWithAgent); + + expect(verifierSpy.args[0][1]).to.equal(agentForApp); + + verifierSpy.restore(); + }); + it('should be fulfilled with decoded claims given a valid Firebase JWT token', () => { - mockedRequests.push(mockFetchPublicKeys()); + const verifierStub = sinon.stub(PublicKeySignatureVerifier.prototype, 'verify') + .resolves(); + stubs.push(verifierStub); clock = sinon.useFakeTimers(1000); @@ -535,109 +475,44 @@ describe('FirebaseTokenVerifier', () => { }); }); - it('should use the given HTTP Agent', () => { - const agent = new https.Agent(); - const appWithAgent = mocks.appWithOptions({ - credential: mocks.credential, - httpAgent: agent, - }); - tokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - appWithAgent, - ); - mockedRequests.push(mockFetchPublicKeys()); - + it('should decode an unsigned token if isEmulator=true', async () => { clock = sinon.useFakeTimers(1000); - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .then(() => { - expect(https.request).to.have.been.calledOnce; - expect(httpsSpy.args[0][0].agent).to.equal(agent); - }); - }); - - it('should not fetch the Google cert public keys until the first time verifyJWT() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const testTokenVerifier = new verifier.FirebaseTokenVerifier( - 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com', - 'RS256', - 'https://securetoken.google.com/', - verifier.ID_TOKEN_INFO, - app, - ); - expect(https.request).not.to.have.been.called; - - const mockIdToken = mocks.generateIdToken(); - - return testTokenVerifier.verifyJWT(mockIdToken) - .then(() => expect(https.request).to.have.been.calledOnce); - }); - - it('should not re-fetch the Google cert public keys every time verifyJWT() is called', () => { - mockedRequests.push(mockFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); + const emulatorVerifier = createTokenVerifier(app); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - expect(https.request).to.have.been.calledOnce; - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => expect(https.request).to.have.been.calledOnce); + 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 refresh the Google cert public keys after the "max-age" on the request expires', () => { - mockedRequests.push(mockFetchPublicKeys()); - mockedRequests.push(mockFetchPublicKeys()); - mockedRequests.push(mockFetchPublicKeys()); - + it('should not decode an unsigned token when the algorithm is not overridden (emulator)', async () => { clock = sinon.useFakeTimers(1000); - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken).then(() => { - expect(https.request).to.have.been.calledOnce; - clock!.tick(999); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - expect(https.request).to.have.been.calledOnce; - clock!.tick(1); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - // One second has passed - expect(https.request).to.have.been.calledTwice; - clock!.tick(999); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - expect(https.request).to.have.been.calledTwice; - clock!.tick(1); - return tokenVerifier.verifyJWT(mockIdToken); - }).then(() => { - // Two seconds have passed - expect(https.request).to.have.been.calledThrice; + const idTokenNoAlg = mocks.generateIdToken({ + algorithm: 'none', }); - }); + await tokenVerifier.verifyJWT(idTokenNoAlg) + .should.eventually.be.rejectedWith('Firebase ID token has incorrect algorithm.'); - it('should be rejected if fetching the Google public keys fails', () => { - mockedRequests.push(mockFailedFetchPublicKeys()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('message'); - }); - - it('should be rejected if fetching the Google public keys returns a response with an error message', () => { - mockedRequests.push(mockFetchPublicKeysWithErrorResponse()); - - const mockIdToken = mocks.generateIdToken(); - - return tokenVerifier.verifyJWT(mockIdToken) - .should.eventually.be.rejectedWith('Error fetching public keys for Google certs: message (description)'); + const idTokenNoHeader = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + await tokenVerifier.verifyJWT(idTokenNoHeader) + .should.eventually.be.rejectedWith('Firebase ID token has no "kid" claim.'); }); }); }); diff --git a/test/unit/auth/user-import-builder.spec.ts b/test/unit/auth/user-import-builder.spec.ts old mode 100755 new mode 100644 index b94e22f1f4..14deeaeb64 --- a/test/unit/auth/user-import-builder.spec.ts +++ b/test/unit/auth/user-import-builder.spec.ts @@ -18,14 +18,17 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {deepCopy} from '../../../src/utils/deep-copy'; +import { deepCopy } from '../../../src/utils/deep-copy'; import { - UserImportBuilder, ValidatorFunction, UserImportResult, UserImportRecord, - UploadAccountRequest, + UserImportBuilder, ValidatorFunction, UploadAccountRequest, } from '../../../src/auth/user-import-builder'; -import {AuthClientErrorCode, FirebaseAuthError} from '../../../src/utils/error'; -import {toWebSafeBase64} from '../../../src/utils'; +import { AuthClientErrorCode, FirebaseAuthError } from '../../../src/utils/error'; +import { toWebSafeBase64 } from '../../../src/utils'; +import { auth } from '../../../src/auth/index'; +import UpdatePhoneMultiFactorInfoRequest = auth.UpdatePhoneMultiFactorInfoRequest; +import UserImportResult = auth.UserImportResult; +import UserImportRecord = auth.UserImportRecord; chai.should(); chai.use(sinonChai); @@ -33,6 +36,17 @@ chai.use(chaiAsPromised); const expect = chai.expect; +export function expectUserImportResult(result: UserImportResult, expected: UserImportResult): void { + expect(result.successCount).to.equal(expected.successCount); + expect(result.failureCount).to.equal(expected.failureCount); + expect(result.errors.length).to.equal(expected.errors.length); + result.errors.forEach((err, idx) => { + const want = expected.errors[idx]; + expect(err.index).to.equal(want.index); + expect(err.error).to.deep.include(want.error); + }); +} + describe('UserImportBuilder', () => { const now = new Date('2019-10-25T04:30:52.000Z'); const nowString = now.toUTCString(); @@ -69,7 +83,7 @@ describe('UserImportBuilder', () => { providerId: 'google.com', }, ], - customClaims: {admin: true}, + customClaims: { admin: true }, tenantId: 'TENANT-ID', }, { @@ -78,7 +92,7 @@ describe('UserImportBuilder', () => { passwordHash: Buffer.from('userpass'), passwordSalt: Buffer.from('NaCl'), }, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '5678', phoneNumber: '+16505550101' }, { uid: '3456', email: 'janedoe@example.com', @@ -122,7 +136,7 @@ describe('UserImportBuilder', () => { providerId: 'google.com', }, ], - customAttributes: JSON.stringify({admin: true}), + customAttributes: JSON.stringify({ admin: true }), tenantId: 'TENANT-ID', }, { @@ -186,7 +200,7 @@ describe('UserImportBuilder', () => { it('should throw when an invalid hash algorithm is provided', () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ALGORITHM, - `Unsupported hash algorithm provider "invalid".`, + 'Unsupported hash algorithm provider "invalid".', ); const invalidOptions = { hash: { @@ -200,8 +214,8 @@ describe('UserImportBuilder', () => { it('should not throw when no hash options are provided and no hashing is needed', () => { const noHashUsers = [ - {uid: '1234', email: 'user@example.com'}, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '1234', email: 'user@example.com' }, + { uid: '5678', phoneNumber: '+16505550101' }, ]; expect(() => { return new UserImportBuilder(noHashUsers, undefined, userRequestValidator); @@ -215,7 +229,7 @@ describe('UserImportBuilder', () => { it(`should throw when non-Buffer ${JSON.stringify(key)} hash key is provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A non-empty "hash.key" byte buffer must be provided for ` + + 'A non-empty "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -323,7 +337,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(key)} key provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_KEY, - `A "hash.key" byte buffer must be provided for ` + + 'A "hash.key" byte buffer must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -344,7 +358,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(rounds)} rounds provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_ROUNDS, - `A valid "hash.rounds" number between 1 and 8 must be provided for ` + + 'A valid "hash.rounds" number between 1 and 8 must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -365,7 +379,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number between 1 and 14 must be provided for ` + + 'A valid "hash.memoryCost" number between 1 and 14 must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -386,7 +400,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(saltSeparator)} saltSeparator provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_SALT_SEPARATOR, - `"hash.saltSeparator" must be a byte buffer.`, + '"hash.saltSeparator" must be a byte buffer.', ); const invalidOptions = { hash: { @@ -454,7 +468,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(memoryCost)} memoryCost provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.memoryCost" number must be provided for ` + + 'A valid "hash.memoryCost" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -476,7 +490,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(parallelization)} parallelization provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_MEMORY_COST, - `A valid "hash.parallelization" number must be provided for ` + + 'A valid "hash.parallelization" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -498,7 +512,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(blockSize)} blockSize provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_BLOCK_SIZE, - `A valid "hash.blockSize" number must be provided for ` + + 'A valid "hash.blockSize" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -520,7 +534,7 @@ describe('UserImportBuilder', () => { it(`should throw when ${JSON.stringify(derivedKeyLength)} dkLen provided`, () => { const expectedError = new FirebaseAuthError( AuthClientErrorCode.INVALID_HASH_DERIVED_KEY_LENGTH, - `A valid "hash.derivedKeyLength" number must be provided for ` + + 'A valid "hash.derivedKeyLength" number must be provided for ' + `hash algorithm ${algorithm}.`, ); const invalidOptions = { @@ -594,7 +608,7 @@ describe('UserImportBuilder', () => { } as any, ); testUsers.push( - {uid: 'INVALID2', email: 'other@domain.com', passwordHash: 'not a buffer'} as any, + { uid: 'INVALID2', email: 'other@domain.com', passwordHash: 'not a buffer' } as any, ); const expectedRequest = { hashAlgorithm: algorithm, @@ -610,13 +624,13 @@ describe('UserImportBuilder', () => { it('should return expected request with no hash options when not required', () => { const noHashUsers = [ - {uid: '1234', email: 'user@example.com'}, - {uid: '5678', phoneNumber: '+16505550101'}, + { uid: '1234', email: 'user@example.com' }, + { uid: '5678', phoneNumber: '+16505550101' }, ]; const expectedRequest = { users: [ - {localId: '1234', email: 'user@example.com'}, - {localId: '5678', phoneNumber: '+16505550101'}, + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, ], }; const userImportBuilder = @@ -626,13 +640,13 @@ describe('UserImportBuilder', () => { it('should return expected request with no multi-factor fields when not available', () => { const noMultiFactorUsers: any[] = [ - {uid: '1234', email: 'user@example.com', multiFactor: null}, - {uid: '5678', phoneNumber: '+16505550101', multiFactor: {enrolledFactors: []}}, + { uid: '1234', email: 'user@example.com', multiFactor: null }, + { uid: '5678', phoneNumber: '+16505550101', multiFactor: { enrolledFactors: [] } }, ]; const expectedRequest = { users: [ - {localId: '1234', email: 'user@example.com'}, - {localId: '5678', phoneNumber: '+16505550101'}, + { localId: '1234', email: 'user@example.com' }, + { localId: '5678', phoneNumber: '+16505550101' }, ], }; const userImportBuilder = @@ -641,26 +655,25 @@ describe('UserImportBuilder', () => { }); it('should ignore users with invalid second factor enrollment time', () => { + const phoneFactor: UpdatePhoneMultiFactorInfoRequest = { + uid: 'enrolledSecondFactor1', + phoneNumber: '+16505557348', + displayName: 'Spouse\'s phone number', + factorId: 'phone', + enrollmentTime: 'invalid', + }; const invalidMultiFactorUsers: UserImportRecord[] = [ { uid: '1234', multiFactor: { - enrolledFactors: [ - { - uid: 'enrolledSecondFactor1', - phoneNumber: '+16505557348', - displayName: 'Spouse\'s phone number', - factorId: 'phone', - enrollmentTime: 'invalid', - }, - ], + enrolledFactors: [ phoneFactor ], }, }, - {uid: '5678', phoneNumber: '+16505550102'}, + { uid: '5678', phoneNumber: '+16505550102' }, ]; const expectedRequest: UploadAccountRequest = { users: [ - {localId: '5678', phoneNumber: '+16505550102'}, + { localId: '5678', phoneNumber: '+16505550102' }, ], }; const userImportBuilder = @@ -683,11 +696,11 @@ describe('UserImportBuilder', () => { ], }, }, - {uid: '5678', phoneNumber: '+16505550102'}, + { uid: '5678', phoneNumber: '+16505550102' }, ]; const expectedRequest: UploadAccountRequest = { users: [ - {localId: '5678', phoneNumber: '+16505550102'}, + { localId: '5678', phoneNumber: '+16505550102' }, ], }; const userImportBuilder = @@ -713,13 +726,14 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidator); - expect(userImportBuilder.buildResponse(successfulServerResponse)) - .to.deep.equal(successfulUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + successfulUserImportResponse); }); it('should return the expected response for import with server side errors', () => { const failingServerResponse = [ - {index: 1, message: 'Some error occurred!'}, + { index: 1, message: 'Some error occurred!' }, ]; const serverErrorUserImportResponse = { successCount: 3, @@ -737,31 +751,33 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidator); - expect(userImportBuilder.buildResponse(failingServerResponse)) - .to.deep.equal(serverErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + serverErrorUserImportResponse); }); it('should return the expected response for import with client side errors', () => { const successfulServerResponse: any = []; - const clientErrorUserImportResponse: UserImportResult = { + const clientErrorUserImportResponse = { successCount: 3, failureCount: 1, errors: [ - {index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + { index: 2, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, ], }; // userRequestValidatorWithError will throw on the 3rd user (index = 2). const userImportBuilder = new UserImportBuilder(users, validOptions as any, userRequestValidatorWithError); - expect(userImportBuilder.buildResponse(successfulServerResponse)) - .to.deep.equal(clientErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(successfulServerResponse), + clientErrorUserImportResponse); }); it('should return the expected response for import with mixed client/server errors', () => { // Server errors will occur on USER3 and USER6 passed to backend. const failingServerResponse = [ - {index: 1, message: 'Some error occurred in USER3!'}, - {index: 3, message: 'Another error occurred in USER6!'}, + { index: 1, message: 'Some error occurred in USER3!' }, + { index: 3, message: 'Another error occurred in USER6!' }, ]; const userRequestValidatorWithMultipleErrors: ValidatorFunction = (request) => { // Simulate a validation error is thrown for specific users. @@ -777,13 +793,13 @@ describe('UserImportBuilder', () => { // Seventh, eighth and nineth user will throw a client side error due to invalid type provided. // Tenth user will throw a client side error due to an unsupported second factor. const testUsers = [ - {uid: 'USER1'}, - {uid: 'USER2', email: 'invalid', passwordHash: Buffer.from('userpass')}, - {uid: 'USER3'}, - {uid: 'USER4', email: 'user@example.com', phoneNumber: 'invalid'}, - {uid: 'USER5', email: 'johndoe@example.com', passwordHash: Buffer.from('password')}, - {uid: 'USER6', phoneNumber: '+16505550101'}, - {uid: 'USER7', email: 'other@domain.com', passwordHash: 'not a buffer' as any}, + { uid: 'USER1' }, + { uid: 'USER2', email: 'invalid', passwordHash: Buffer.from('userpass') }, + { uid: 'USER3' }, + { uid: 'USER4', email: 'user@example.com', phoneNumber: 'invalid' }, + { uid: 'USER5', email: 'johndoe@example.com', passwordHash: Buffer.from('password') }, + { uid: 'USER6', phoneNumber: '+16505550101' }, + { uid: 'USER7', email: 'other@domain.com', passwordHash: 'not a buffer' as any }, { uid: 'USER8', email: 'other@domain.com', @@ -822,7 +838,7 @@ describe('UserImportBuilder', () => { failureCount: 8, errors: [ // Client side detected error. - {index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL)}, + { index: 1, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL) }, // Server side detected error. { index: 2, @@ -832,7 +848,7 @@ describe('UserImportBuilder', () => { ), }, // Client side detected error. - {index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER)}, + { index: 3, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PHONE_NUMBER) }, // Server side detected error. { index: 5, @@ -842,14 +858,14 @@ describe('UserImportBuilder', () => { ), }, // Client side errors. - {index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH)}, - {index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT)}, + { index: 6, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_HASH) }, + { index: 7, error: new FirebaseAuthError(AuthClientErrorCode.INVALID_PASSWORD_SALT) }, { index: 8, error: new FirebaseAuthError( AuthClientErrorCode.INVALID_ENROLLMENT_TIME, - `The second factor "enrollmentTime" for "enrollmentId1" must be a valid ` + - `UTC date string.`), + 'The second factor "enrollmentTime" for "enrollmentId1" must be a valid ' + + 'UTC date string.'), }, { index: 9, @@ -861,9 +877,9 @@ describe('UserImportBuilder', () => { }; const userImportBuilder = new UserImportBuilder( testUsers, validOptions as any, userRequestValidatorWithMultipleErrors); - expect(userImportBuilder.buildResponse(failingServerResponse)) - .to.deep.equal(mixedErrorUserImportResponse); + expectUserImportResult( + userImportBuilder.buildResponse(failingServerResponse), + mixedErrorUserImportResponse); }); }); - }); diff --git a/test/unit/auth/user-record.spec.ts b/test/unit/auth/user-record.spec.ts index af1ae9773b..0a42de92cb 100644 --- a/test/unit/auth/user-record.spec.ts +++ b/test/unit/auth/user-record.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +19,7 @@ import * as chai from 'chai'; import * as sinonChai from 'sinon-chai'; import * as chaiAsPromised from 'chai-as-promised'; -import {deepCopy} from '../../../src/utils/deep-copy'; +import { deepCopy } from '../../../src/utils/deep-copy'; import { UserInfo, UserMetadata, UserRecord, GetAccountInfoUserResponse, ProviderUserInfoResponse, MultiFactor, PhoneMultiFactorInfo, MultiFactorInfo, MultiFactorInfoResponse, @@ -173,7 +174,7 @@ function getUserJSON(tenantId?: string): object { }, { uid: 'enrollmentId2', - displayName: null, + displayName: undefined, enrollmentTime: now.toUTCString(), phoneNumber: '+16505556789', factorId: 'phone', @@ -294,7 +295,7 @@ describe('PhoneMultiFactorInfo', () => { describe('getters', () => { it('should set missing optional fields to null', () => { expect(phoneMultiFactorInfoMissingFields.uid).to.equal(serverResponse.mfaEnrollmentId); - expect(phoneMultiFactorInfoMissingFields.displayName).to.be.null; + expect(phoneMultiFactorInfoMissingFields.displayName).to.be.undefined; expect(phoneMultiFactorInfoMissingFields.phoneNumber).to.equal(serverResponse.phoneInfo); expect(phoneMultiFactorInfoMissingFields.enrollmentTime).to.be.null; expect(phoneMultiFactorInfoMissingFields.factorId).to.equal('phone'); @@ -365,7 +366,7 @@ describe('PhoneMultiFactorInfo', () => { it('should return expected JSON object with missing fields set to null', () => { expect(phoneMultiFactorInfoMissingFields.toJSON()).to.deep.equal({ uid: 'enrollmentId1', - displayName: null, + displayName: undefined, enrollmentTime: null, phoneNumber: '+16505551234', factorId: 'phone', @@ -515,19 +516,19 @@ describe('UserInfo', () => { it('should succeed when rawId and providerId are both provided', () => { expect(() => { - return new UserInfo({providerId: 'google.com', rawId: '1234567890'}); + return new UserInfo({ providerId: 'google.com', rawId: '1234567890' }); }).not.to.throw(Error); }); it('should throw when only rawId is provided', () => { expect(() => { - return new UserInfo({rawId: '1234567890'} as any); + return new UserInfo({ rawId: '1234567890' } as any); }).to.throw(Error); }); it('should throw when only providerId is provided', () => { expect(() => { - return new UserInfo({providerId: 'google.com'} as any); + return new UserInfo({ providerId: 'google.com' } as any); }).to.throw(Error); }); }); @@ -631,7 +632,7 @@ describe('UserMetadata', () => { describe('constructor', () => { it('should initialize as expected when a valid creationTime is provided', () => { expect(() => { - return new UserMetadata({createdAt: '1476136676000'} as any); + return new UserMetadata({ createdAt: '1476136676000' } as any); }).not.to.throw(Error); }); @@ -701,7 +702,7 @@ describe('UserRecord', () => { it('should succeed when only localId is provided', () => { expect(() => { - return new UserRecord({localId: '123456789'}); + return new UserRecord({ localId: '123456789' }); }).not.to.throw(Error); }); }); @@ -849,7 +850,7 @@ describe('UserRecord', () => { it('should throw when modifying readonly customClaims property', () => { expect(() => { - (userRecord as any).customClaims = {admin: false}; + (userRecord as any).customClaims = { admin: false }; }).to.throw(Error); }); diff --git a/test/unit/auth/credential.spec.ts b/test/unit/credential/credential.spec.ts similarity index 98% rename from test/unit/auth/credential.spec.ts rename to test/unit/credential/credential.spec.ts index 881dcb7525..d34b569bda 100644 --- a/test/unit/auth/credential.spec.ts +++ b/test/unit/credential/credential.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,11 +32,14 @@ import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { - GoogleOAuthAccessToken, RefreshTokenCredential, ServiceAccountCredential, - ComputeEngineCredential, getApplicationDefault, isApplicationDefault, Credential, -} from '../../../src/auth/credential'; + GoogleOAuthAccessToken, credential +} from '../../../src/credential/index'; +import { + RefreshTokenCredential, ServiceAccountCredential, + ComputeEngineCredential, getApplicationDefault, isApplicationDefault +} from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; -import {Agent} from 'https'; +import { Agent } from 'https'; import { FirebaseAppError } from '../../../src/utils/error'; chai.should(); @@ -325,7 +329,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); @@ -342,7 +346,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); @@ -364,7 +368,7 @@ describe('Credential', () => { expect(httpStub).to.have.been.calledOnce.and.calledWith({ method: 'GET', url: 'http://metadata.google.internal/computeMetadata/v1/project/project-id', - headers: {'Metadata-Flavor': 'Google'}, + headers: { 'Metadata-Flavor': 'Google' }, httpAgent: undefined, }); }); @@ -552,7 +556,7 @@ describe('Credential', () => { }); it('should return false for custom credential', () => { - const c: Credential = { + const c: credential.Credential = { getAccessToken: () => { throw new Error(); }, diff --git a/test/unit/database/database.spec.ts b/test/unit/database/database.spec.ts index 54d59432a3..763041262e 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,16 +18,19 @@ 'use strict'; import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {DatabaseService} from '../../../src/database/database'; -import {Database} from '@firebase/database'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { DatabaseService } from '../../../src/database/database-internal'; +import { database } from '../../../src/database/index'; +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; import * as utils from '../utils'; import { HttpClient, HttpRequestConfig } from '../../../src/utils/api-request'; +import Database = database.Database; + describe('Database', () => { let mockApp: FirebaseApp; let database: DatabaseService; @@ -37,7 +41,7 @@ describe('Database', () => { }); afterEach(() => { - return database.INTERNAL.delete().then(() => { + return database.delete().then(() => { return mockApp.delete(); }); }); @@ -45,7 +49,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); @@ -115,6 +119,179 @@ describe('Database', () => { }); }); + describe('Token refresh', () => { + const MINUTE_IN_MILLIS = 60 * 1000; + + let clock: sinon.SinonFakeTimers; + let getTokenStub: sinon.SinonStub; + + beforeEach(() => { + getTokenStub = stubCredentials(); + clock = sinon.useFakeTimers(1000); + }); + + afterEach(() => { + getTokenStub.restore(); + clock.restore(); + }); + + function stubCredentials(options?: { + accessToken?: string; + expiresIn?: number; + err?: any; + }): sinon.SinonStub { + if (options?.err) { + return sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .rejects(options.err); + } + + return sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken') + .resolves({ + access_token: options?.accessToken || 'mock-access-token', // eslint-disable-line @typescript-eslint/camelcase + expires_in: options?.expiresIn || 3600, // eslint-disable-line @typescript-eslint/camelcase + }); + } + + it('should refresh the token 5 minutes before expiration', () => { + database.getDatabase(); + 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; @@ -151,11 +328,8 @@ describe('Database', () => { }`; const rulesPath = '.settings/rules.json'; - function callParamsForGet( - strict = false, - url = `https://databasename.firebaseio.com/${rulesPath}`, - ): HttpRequestConfig { - + function callParamsForGet(options?: { strict?: boolean; url?: string }): HttpRequestConfig { + const url = options?.url || `https://databasename.firebaseio.com/${rulesPath}`; const params: HttpRequestConfig = { method: 'GET', url, @@ -164,8 +338,8 @@ describe('Database', () => { }, }; - if (strict) { - params.data = {format: 'strict'}; + if (options?.strict) { + params.data = { format: 'strict' }; } return params; @@ -212,7 +386,7 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -222,13 +396,13 @@ describe('Database', () => { return db.getRules().then((result) => { expect(result).to.equal(rulesString); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(false, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.getRules().should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); @@ -256,7 +430,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true)); + callParamsForGet({ strict: true })); }); }); @@ -266,7 +440,7 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `https://custom.firebaseio.com/${rulesPath}`)); + callParamsForGet({ strict: true, url: `https://custom.firebaseio.com/${rulesPath}` })); }); }); @@ -276,13 +450,13 @@ describe('Database', () => { return db.getRulesJSON().then((result) => { expect(result).to.deep.equal(rules); return expect(stub).to.have.been.calledOnce.and.calledWith( - callParamsForGet(true, `http://localhost:9000/${rulesPath}?ns=foo`)); + callParamsForGet({ strict: true, url: `http://localhost:9000/${rulesPath}?ns=foo` })); }); }); it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.getRulesJSON().should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); @@ -386,7 +560,7 @@ describe('Database', () => { it('should throw if the server responds with a well-formed error', () => { const db: Database = database.getDatabase(); - stubErrorResponse({error: 'test error'}); + stubErrorResponse({ error: 'test error' }); return db.setRules(rules).should.eventually.be.rejectedWith( 'Error while accessing security rules: test error'); }); @@ -406,5 +580,101 @@ describe('Database', () => { return db.setRules(rules).should.eventually.be.rejectedWith('network error'); }); }); + + describe('emulator mode', () => { + interface EmulatorTestConfig { + name: string; + setUp: () => FirebaseApp; + tearDown?: () => void; + url: string; + } + + const configs: EmulatorTestConfig[] = [ + { + name: 'with environment variable', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.app(); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + { + name: 'with app options', + setUp: () => { + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + url: `http://localhost:9091/${rulesPath}?ns=databasename`, + }, + { + name: 'with environment variable overriding app options', + setUp: () => { + process.env.FIREBASE_DATABASE_EMULATOR_HOST = 'localhost:9090'; + return mocks.appWithOptions({ + databaseURL: 'http://localhost:9091?ns=databasename', + }); + }, + tearDown: () => { + delete process.env.FIREBASE_DATABASE_EMULATOR_HOST; + }, + url: `http://localhost:9090/${rulesPath}?ns=databasename`, + }, + ]; + + configs.forEach((config) => { + describe(config.name, () => { + let emulatorApp: FirebaseApp; + let emulatorDatabase: DatabaseService; + + before(() => { + emulatorApp = config.setUp(); + emulatorDatabase = new DatabaseService(emulatorApp); + }); + + after(() => { + if (config.tearDown) { + config.tearDown(); + } + + return emulatorDatabase.delete().then(() => { + return emulatorApp.delete(); + }); + }); + + it('getRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRules().then((result) => { + expect(result).to.equal(rulesString); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ url: config.url })); + }); + }); + + it('getRulesJSON should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse(rules); + return db.getRulesJSON().then((result) => { + expect(result).to.equal(rules); + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForGet({ strict: true, url: config.url })); + }); + }); + + it('setRules should connect to the emulator', () => { + const db: Database = emulatorDatabase.getDatabase(); + const stub = stubSuccessfulResponse({}); + return db.setRules(rulesString).then(() => { + return expect(stub).to.have.been.calledOnce.and.calledWith( + callParamsForPut(rulesString, config.url)); + }); + }); + }); + }); + }); }); }); diff --git a/test/unit/firebase-app.spec.ts b/test/unit/firebase-app.spec.ts index ef63754993..1929637f94 100644 --- a/test/unit/firebase-app.spec.ts +++ b/test/unit/firebase-app.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,22 +26,37 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from './utils'; import * as mocks from '../resources/mocks'; -import {GoogleOAuthAccessToken, ServiceAccountCredential} from '../../src/auth/credential'; -import {FirebaseServiceInterface} from '../../src/firebase-service'; -import {FirebaseApp, FirebaseAccessToken} from '../../src/firebase-app'; -import {FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR} from '../../src/firebase-namespace'; - -import {Auth} from '../../src/auth/auth'; -import {Messaging} from '../../src/messaging/messaging'; -import {MachineLearning} from '../../src/machine-learning/machine-learning'; -import {Storage} from '../../src/storage/storage'; -import {Firestore} from '@google-cloud/firestore'; -import {Database} from '@firebase/database'; -import {InstanceId} from '../../src/instance-id/instance-id'; -import {ProjectManagement} from '../../src/project-management/project-management'; -import { SecurityRules } from '../../src/security-rules/security-rules'; +import { GoogleOAuthAccessToken } from '../../src/credential/index'; +import { ServiceAccountCredential } from '../../src/credential/credential-internal'; +import { FirebaseApp, FirebaseAccessToken } from '../../src/firebase-app'; +import { FirebaseNamespace, FirebaseNamespaceInternals, FIREBASE_CONFIG_VAR } from '../../src/firebase-namespace'; + +import { auth } from '../../src/auth/index'; +import { messaging } from '../../src/messaging/index'; +import { machineLearning } from '../../src/machine-learning/index'; +import { storage } from '../../src/storage/index'; +import { firestore } from '../../src/firestore/index'; +import { database } from '../../src/database/index'; +import { installations } from '../../src/installations/index'; +import { instanceId } from '../../src/instance-id/index'; +import { projectManagement } from '../../src/project-management/index'; +import { securityRules } from '../../src/security-rules/index'; +import { remoteConfig } from '../../src/remote-config/index'; +import { appCheck } from '../../src/app-check/index'; import { FirebaseAppError, AppErrorCodes } from '../../src/utils/error'; -import { RemoteConfig } from '../../src/remote-config/remote-config'; + +import Auth = auth.Auth; +import Database = database.Database; +import Messaging = messaging.Messaging; +import MachineLearning = machineLearning.MachineLearning; +import Storage = storage.Storage; +import Firestore = firestore.Firestore; +import 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); @@ -52,13 +68,14 @@ const ONE_HOUR_IN_SECONDS = 60 * 60; const ONE_MINUTE_IN_MILLISECONDS = 60 * 1000; const deleteSpy = sinon.spy(); -function mockServiceFactory(app: FirebaseApp): FirebaseServiceInterface { - return { - app, - INTERNAL: { - delete: deleteSpy.bind(null, app.name), - }, - }; + +class TestService { + public deleted = false; + + public delete(): Promise { + this.deleted = true; + return Promise.resolve(); + } } @@ -77,9 +94,6 @@ describe('FirebaseApp', () => { }); clock = sinon.useFakeTimers(1000); - - mockApp = mocks.app(); - firebaseConfigVar = process.env[FIREBASE_CONFIG_VAR]; delete process.env[FIREBASE_CONFIG_VAR]; firebaseNamespace = new FirebaseNamespace(); @@ -132,7 +146,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'); }); }); @@ -152,7 +166,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', () => { @@ -174,28 +188,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', () => { @@ -329,18 +343,15 @@ describe('FirebaseApp', () => { }); it('should call delete() on each service\'s internals', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName + '2', mockServiceFactory); - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - (app as {[key: string]: any})[mocks.serviceName](); - (app as {[key: string]: any})[mocks.serviceName + '2'](); + const svc1 = new TestService(); + const svc2 = new TestService(); + (app as any).ensureService_(mocks.serviceName, () => svc1); + (app as any).ensureService_(mocks.serviceName + '2', () => svc2); return app.delete().then(() => { - expect(deleteSpy).to.have.been.calledTwice; - expect(deleteSpy.firstCall.args).to.deep.equal([mocks.appName]); - expect(deleteSpy.secondCall.args).to.deep.equal([mocks.appName]); + expect(svc1.deleted).to.be.true; + expect(svc2.deleted).to.be.true; }); }); }); @@ -558,6 +569,32 @@ describe('FirebaseApp', () => { }); }); + describe('installations()', () => { + it('should throw if the app has already been deleted', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + return app.delete().then(() => { + expect(() => { + return app.installations(); + }).to.throw(`Firebase app named "${mocks.appName}" has already been deleted.`); + }); + }); + + it('should return the InstanceId client', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + + const fis: Installations = app.installations(); + expect(fis).not.be.null; + }); + + it('should return a cached version of InstanceId on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); + const service1: Installations = app.installations(); + const service2: Installations = app.installations(); + expect(service1).to.equal(service2); + }); + }); + describe('instanceId()', () => { it('should throw if the app has already been deleted', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); @@ -662,42 +699,29 @@ describe('FirebaseApp', () => { }); }); - describe('#[service]()', () => { + describe('appCheck()', () => { it('should throw if the app has already been deleted', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mockServiceFactory); - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); return app.delete().then(() => { expect(() => { - return (app as {[key: string]: any})[mocks.serviceName](); + return app.appCheck(); }).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 AppCheck client', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - const serviceNamespace = (app as {[key: string]: any})[mocks.serviceName](); - expect(serviceNamespace).to.have.keys(['app', 'INTERNAL']); + const appCheck: AppCheck = app.appCheck(); + expect(appCheck).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 AppCheck on subsequent calls', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - expect(createServiceSpy).to.not.have.been.called; - - const serviceNamespace1 = (app as {[key: string]: any})[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; - - const serviceNamespace2 = (app as {[key: string]: any})[mocks.serviceName](); - expect(createServiceSpy).to.have.been.calledOnce; - expect(serviceNamespace1).to.deep.equal(serviceNamespace2); + const service1: AppCheck = app.appCheck(); + const service2: AppCheck = app.appCheck(); + expect(service1).to.equal(service2); }); }); @@ -728,7 +752,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); @@ -796,204 +820,6 @@ describe('FirebaseApp', () => { }); }); - it('retries to proactively refresh the token if a proactive refresh attempt fails', () => { - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - // Stub the getToken() method to return a rejected promise. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') - .rejects(new Error('Intentionally rejected')); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token1.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - // Forward the clock to exactly four minutes before expiry. - clock.tick(60 * 1000); - - // Restore the stubbed getAccessToken() method. - getTokenStub.restore(); - getTokenStub = sinon.stub(ServiceAccountCredential.prototype, 'getAccessToken').resolves({ - access_token: 'mock-access-token', // eslint-disable-line @typescript-eslint/camelcase - expires_in: 3600, // eslint-disable-line @typescript-eslint/camelcase - }); - - return mockApp.INTERNAL.getToken().then((token2) => { - // Ensure the token has not been proactively refreshed. - expect(token1).to.deep.equal(token2); - expect(getTokenStub).to.have.not.been.called; - - // Forward the clock to exactly three minutes before expiry. - clock.tick(60 * 1000); - - return mockApp.INTERNAL.getToken().then((token3) => { - // Ensure the token was proactively refreshed. - expect(token1).to.not.deep.equal(token3); - expect(getTokenStub).to.have.been.calledOnce; - }); - }); - }); - }); - - it('stops retrying to proactively refresh the token after five attempts', () => { - // Force a token refresh. - let originalToken: FirebaseAccessToken; - return mockApp.INTERNAL.getToken(true).then((token) => { - originalToken = token; - - // Stub the credential's getAccessToken() method to always return a rejected promise. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken') - .rejects(new Error('Intentionally rejected')); - - // Expect the call count to initially be zero. - expect(getTokenStub.callCount).to.equal(0); - - // Forward the clock to exactly five minutes before expiry. - const expiryInMilliseconds = token.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - // Due to synchronous timing issues when the timer is mocked, make a call to getToken() - // without forcing a refresh to ensure there is enough time for the underlying token refresh - // timeout to fire and complete. - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed one time. - expect(getTokenStub.callCount).to.equal(1); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to four minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed two times. - expect(getTokenStub.callCount).to.equal(2); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to three minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed three times. - expect(getTokenStub.callCount).to.equal(3); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to two minutes before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed four times. - expect(getTokenStub.callCount).to.equal(4); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to one minute before expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was attempted to be proactively refreshed five times. - expect(getTokenStub.callCount).to.equal(5); - - // Ensure the proactive refresh failed. - expect(token).to.deep.equal(originalToken); - - // Forward the clock to expiry. - clock.tick(ONE_MINUTE_IN_MILLISECONDS); - - // See note above about calling getToken(). - return mockApp.INTERNAL.getToken(); - }).then((token) => { - // Ensure the token was not attempted to be proactively refreshed a sixth time. - expect(getTokenStub.callCount).to.equal(5); - - // Ensure the token has never been refresh. - expect(token).to.deep.equal(originalToken); - }); - }); - - it('resets the proactive refresh timeout upon a force refresh', () => { - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - // Forward the clock to five minutes and one second before expiry. - let expiryInMilliseconds = token1.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS) - 1000); - - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token2) => { - // Ensure the token was force refreshed. - expect(token1).to.not.deep.equal(token2); - expect(getTokenStub).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the original token's expiry. - clock.tick(1000); - - return mockApp.INTERNAL.getToken().then((token3) => { - // Ensure the token hasn't changed, meaning the proactive refresh was canceled. - expect(token2).to.deep.equal(token3); - expect(getTokenStub).to.have.been.calledTwice; - - // Forward the clock to exactly five minutes before the refreshed token's expiry. - expiryInMilliseconds = token3.expirationTime - Date.now(); - clock.tick(expiryInMilliseconds - (5 * ONE_MINUTE_IN_MILLISECONDS)); - - return mockApp.INTERNAL.getToken().then((token4) => { - // Ensure the token was proactively refreshed. - expect(token3).to.not.deep.equal(token4); - expect(getTokenStub).to.have.been.calledThrice; - }); - }); - }); - }); - }); - - it('proactively refreshes the token at the next full minute if it expires in five minutes or less', () => { - // Turn off default mocking of one hour access tokens and replace it with a short-lived token. - getTokenStub.restore(); - expect(mockApp.options.credential).to.exist; - getTokenStub = sinon.stub(mockApp.options.credential!, 'getAccessToken').resolves({ - access_token: utils.generateRandomAccessToken(), // eslint-disable-line @typescript-eslint/camelcase - expires_in: 3 * 60 + 10, // eslint-disable-line @typescript-eslint/camelcase - }); - // Expect the call count to initially be zero. - expect(getTokenStub.callCount).to.equal(0); - - // Force a token refresh. - return mockApp.INTERNAL.getToken(true).then((token1) => { - - // Move the clock forward to three minutes and one second before expiry. - clock.tick(9 * 1000); - expect(getTokenStub.callCount).to.equal(1); - - // Move the clock forward to exactly three minutes before expiry. - clock.tick(1000); - - // Expect the underlying getAccessToken() method to have been called once. - expect(getTokenStub.callCount).to.equal(2); - - return mockApp.INTERNAL.getToken().then((token2) => { - // Ensure the token was proactively refreshed. - expect(token1).to.not.deep.equal(token2); - }); - }); - }); - it('Includes the original error in exception', () => { getTokenStub.restore(); const mockError = new FirebaseAppError( diff --git a/test/unit/firebase-namespace.spec.ts b/test/unit/firebase-namespace.spec.ts index 97478a330c..45047df4a7 100644 --- a/test/unit/firebase-namespace.spec.ts +++ b/test/unit/firebase-namespace.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,29 +19,23 @@ 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 {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp} from '../../src/firebase-app'; -import {Auth} from '../../src/auth/auth'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; import { enableLogging, - Database, + Database as DatabaseImpl, DataSnapshot, OnDisconnect, Query, Reference, ServerValue, } from '@firebase/database'; -import {Messaging} from '../../src/messaging/messaging'; -import {MachineLearning} from '../../src/machine-learning/machine-learning'; -import {Storage} from '../../src/storage/storage'; + import { - Firestore, FieldPath, FieldValue, GeoPoint, @@ -48,10 +43,46 @@ import { v1beta1, setLogFunction, } from '@google-cloud/firestore'; -import {InstanceId} from '../../src/instance-id/instance-id'; -import {ProjectManagement} from '../../src/project-management/project-management'; -import { SecurityRules } from '../../src/security-rules/security-rules'; -import { RemoteConfig } from '../../src/remote-config/remote-config'; +import { getSdkVersion } from '../../src/utils/index'; + +import { app } from '../../src/firebase-namespace-api'; +import { auth } from '../../src/auth/index'; +import { messaging } from '../../src/messaging/index'; +import { machineLearning } from '../../src/machine-learning/index'; +import { storage } from '../../src/storage/index'; +import { firestore } from '../../src/firestore/index'; +import { database } from '../../src/database/index'; +import { installations } from '../../src/installations/index'; +import { instanceId } from '../../src/instance-id/index'; +import { projectManagement } from '../../src/project-management/index'; +import { securityRules } from '../../src/security-rules/index'; +import { remoteConfig } from '../../src/remote-config/index'; +import { appCheck } from '../../src/app-check/index'; + +import { AppCheck as AppCheckImpl } from '../../src/app-check/app-check'; +import { Auth as AuthImpl } from '../../src/auth/auth'; +import { Installations as InstallationsImpl } from '../../src/installations/installations'; +import { InstanceId as InstanceIdImpl } from '../../src/instance-id/instance-id'; +import { MachineLearning as MachineLearningImpl } from '../../src/machine-learning/machine-learning'; +import { Messaging as MessagingImpl } from '../../src/messaging/messaging'; +import { ProjectManagement as ProjectManagementImpl } from '../../src/project-management/project-management'; +import { RemoteConfig as RemoteConfigImpl } from '../../src/remote-config/remote-config'; +import { SecurityRules as SecurityRulesImpl } from '../../src/security-rules/security-rules'; +import { Storage as StorageImpl } from '../../src/storage/storage'; + +import App = app.App; +import 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); @@ -73,7 +104,7 @@ describe('FirebaseNamespace', () => { describe('#SDK_VERSION', () => { it('should return the SDK version', () => { - expect(firebaseNamespace.SDK_VERSION).to.equal(''); + expect(firebaseNamespace.SDK_VERSION).to.equal(getSdkVersion()); }); }); @@ -108,7 +139,7 @@ describe('FirebaseNamespace', () => { it('should be read-only', () => { expect(() => { (firebaseNamespace as any).apps = 'foo'; - }).to.throw(`Cannot set property apps of # which has only a getter`); + }).to.throw('Cannot set property apps of # which has only a getter'); }); }); @@ -125,7 +156,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { return firebaseNamespace.app(''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given an app name which does not correspond to an existing app', () => { @@ -178,7 +209,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { firebaseNamespace.initializeApp(mocks.appOptions, ''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given a name corresponding to an existing app', () => { @@ -233,15 +264,6 @@ describe('FirebaseNamespace', () => { const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); expect(firebaseNamespace.app(mocks.appName)).to.deep.equal(app); }); - - it('should call the "create" app hook for the new app', () => { - const appHook = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory, undefined, appHook); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - expect(appHook).to.have.been.calledOnce.and.calledWith('create', app); - }); }); describe('#INTERNAL.removeApp()', () => { @@ -257,7 +279,7 @@ describe('FirebaseNamespace', () => { it('should throw given empty string app name', () => { expect(() => { firebaseNamespace.INTERNAL.removeApp(''); - }).to.throw(`Invalid Firebase app name "" provided. App name must be a non-empty string.`); + }).to.throw('Invalid Firebase app name "" provided. App name must be a non-empty string.'); }); it('should throw given an app name which does not correspond to an existing app', () => { @@ -269,14 +291,14 @@ describe('FirebaseNamespace', () => { it('should throw given no app name if the default app does not exist', () => { expect(() => { (firebaseNamespace as any).INTERNAL.removeApp(); - }).to.throw(`No Firebase app name provided. App name must be a non-empty string.`); + }).to.throw('No Firebase app name provided. App name must be a non-empty string.'); }); it('should throw given no app name even if the default app exists', () => { firebaseNamespace.initializeApp(mocks.appOptions); expect(() => { (firebaseNamespace as any).INTERNAL.removeApp(); - }).to.throw(`No Firebase app name provided. App name must be a non-empty string.`); + }).to.throw('No Firebase app name provided. App name must be a non-empty string.'); }); it('should remove the app corresponding to the provided app name from the namespace\'s app list', () => { @@ -302,58 +324,6 @@ describe('FirebaseNamespace', () => { firebaseNamespace.INTERNAL.removeApp(mocks.appName); }).to.throw(`Firebase app named "${mocks.appName}" does not exist.`); }); - - it('should call the "delete" app hook for the deleted app', () => { - const appHook = sinon.spy(); - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory, undefined, appHook); - - const app = firebaseNamespace.initializeApp(mocks.appOptions, mocks.appName); - - appHook.resetHistory(); - - firebaseNamespace.INTERNAL.removeApp(mocks.appName); - - expect(appHook).to.have.been.calledOnce.and.calledWith('delete', app); - }); - }); - - describe('#INTERNAL.registerService()', () => { - // TODO(jwenger): finish writing tests for regsiterService() to get more code coverage - - it('should throw given no service name', () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService(undefined as unknown as string, mocks.firebaseServiceFactory); - }).to.throw(`No service name provided. Service name must be a non-empty string.`); - }); - - const invalidServiceNames = [null, NaN, 0, 1, true, false, [], ['a'], {}, { a: 1 }, _.noop]; - invalidServiceNames.forEach((invalidServiceName) => { - it('should throw given non-string service name: ' + JSON.stringify(invalidServiceName), () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService(invalidServiceName as any, mocks.firebaseServiceFactory); - }).to.throw(`Invalid service name "${invalidServiceName}" provided. Service name must be a non-empty string.`); - }); - }); - - it('should throw given an empty string service name', () => { - expect(() => { - firebaseNamespace.INTERNAL.registerService('', mocks.firebaseServiceFactory); - }).to.throw(`Invalid service name "" provided. Service name must be a non-empty string.`); - }); - - it('should throw given a service name which has already been registered', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - expect(() => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - }).to.throw(`Firebase service named "${mocks.serviceName}" has already been registered.`); - }); - - it('should throw given a service name which has already been registered', () => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - expect(() => { - firebaseNamespace.INTERNAL.registerService(mocks.serviceName, mocks.firebaseServiceFactory); - }).to.throw(`Firebase service named "${mocks.serviceName}" has already been registered.`); - }); }); describe('#auth()', () => { @@ -371,19 +341,26 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const auth: Auth = firebaseNamespace.auth(); expect(auth.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const auth: Auth = firebaseNamespace.auth(app); expect(auth.app).to.be.deep.equal(app); }); it('should return a reference to Auth type', () => { - expect(firebaseNamespace.auth.Auth).to.be.deep.equal(Auth); + expect(firebaseNamespace.auth.Auth).to.be.deep.equal(AuthImpl); + }); + + 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); }); }); @@ -402,21 +379,21 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const 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 app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const db: Database = firebaseNamespace.database(app); expect(db.app).to.be.deep.equal(app); return app.delete(); }); it('should return a reference to Database type', () => { - expect(firebaseNamespace.database.Database).to.be.deep.equal(Database); + expect(firebaseNamespace.database.Database).to.be.deep.equal(DatabaseImpl); }); it('should return a reference to DataSnapshot type', () => { @@ -442,6 +419,14 @@ describe('FirebaseNamespace', () => { it('should return a reference to enableLogging function', () => { expect(firebaseNamespace.database.enableLogging).to.be.deep.equal(enableLogging); }); + + it('should return a cached version of Database on subsequent calls', () => { + const app = firebaseNamespace.initializeApp(mocks.appOptions); + const db1: Database = firebaseNamespace.database(); + const db2: Database = firebaseNamespace.database(); + expect(db1).to.equal(db2); + return app.delete(); + }); }); describe('#messaging()', () => { @@ -459,19 +444,26 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const fcm: Messaging = firebaseNamespace.messaging(); expect(fcm.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const fcm: Messaging = firebaseNamespace.messaging(app); expect(fcm.app).to.be.deep.equal(app); }); it('should return a reference to Messaging type', () => { - expect(firebaseNamespace.messaging.Messaging).to.be.deep.equal(Messaging); + expect(firebaseNamespace.messaging.Messaging).to.be.deep.equal(MessagingImpl); + }); + + 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); }); }); @@ -490,20 +482,27 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const ml: MachineLearning = firebaseNamespace.machineLearning(); expect(ml.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const ml: MachineLearning = firebaseNamespace.machineLearning(app); expect(ml.app).to.be.deep.equal(app); }); it('should return a reference to Machine Learning type', () => { expect(firebaseNamespace.machineLearning.MachineLearning) - .to.be.deep.equal(MachineLearning); + .to.be.deep.equal(MachineLearningImpl); + }); + + 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); }); }); @@ -522,19 +521,26 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const gcs: Storage = firebaseNamespace.storage(); expect(gcs.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const gcs: Storage = firebaseNamespace.storage(app); expect(gcs.app).to.be.deep.equal(app); }); it('should return a reference to Storage type', () => { - expect(firebaseNamespace.storage.Storage).to.be.deep.equal(Storage); + expect(firebaseNamespace.storage.Storage).to.be.deep.equal(StorageImpl); + }); + + 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); }); }); @@ -559,7 +565,7 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const fs: Firestore = firebaseNamespace.firestore(app); expect(fs).to.not.be.null; }); @@ -591,6 +597,53 @@ describe('FirebaseNamespace', () => { it('should return a reference to the v1 namespace', () => { expect(firebaseNamespace.firestore.v1).to.be.deep.equal(v1); }); + + it('should return a cached version of Firestore on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: Firestore = firebaseNamespace.firestore(); + const service2: Firestore = firebaseNamespace.firestore(); + expect(service1).to.equal(service2); + }); + }); + + describe('#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()', () => { @@ -608,21 +661,28 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const iid: InstanceId = firebaseNamespace.instanceId(); expect(iid).to.not.be.null; expect(iid.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const iid: InstanceId = firebaseNamespace.instanceId(app); expect(iid).to.not.be.null; expect(iid.app).to.be.deep.equal(app); }); it('should return a reference to InstanceId type', () => { - expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceId); + expect(firebaseNamespace.instanceId.InstanceId).to.be.deep.equal(InstanceIdImpl); + }); + + 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); }); }); @@ -641,14 +701,14 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const projectManagement: ProjectManagement = firebaseNamespace.projectManagement(); expect(projectManagement).to.not.be.null; expect(projectManagement.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const projectManagement: ProjectManagement = firebaseNamespace.projectManagement(app); expect(projectManagement).to.not.be.null; expect(projectManagement.app).to.be.deep.equal(app); @@ -656,7 +716,14 @@ describe('FirebaseNamespace', () => { it('should return a reference to ProjectManagement type', () => { expect(firebaseNamespace.projectManagement.ProjectManagement) - .to.be.deep.equal(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); }); }); @@ -675,14 +742,14 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const securityRules: SecurityRules = firebaseNamespace.securityRules(); expect(securityRules).to.not.be.null; expect(securityRules.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const securityRules: SecurityRules = firebaseNamespace.securityRules(app); expect(securityRules).to.not.be.null; expect(securityRules.app).to.be.deep.equal(app); @@ -690,7 +757,14 @@ describe('FirebaseNamespace', () => { it('should return a reference to SecurityRules type', () => { expect(firebaseNamespace.securityRules.SecurityRules) - .to.be.deep.equal(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); }); }); @@ -709,19 +783,64 @@ describe('FirebaseNamespace', () => { }); it('should return a valid namespace when the default app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); const rc: RemoteConfig = firebaseNamespace.remoteConfig(); expect(rc.app).to.be.deep.equal(app); }); it('should return a valid namespace when the named app is initialized', () => { - const app: FirebaseApp = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); const rc: RemoteConfig = firebaseNamespace.remoteConfig(app); expect(rc.app).to.be.deep.equal(app); }); it('should return a reference to RemoteConfig type', () => { - expect(firebaseNamespace.remoteConfig.RemoteConfig).to.be.deep.equal(RemoteConfig); + expect(firebaseNamespace.remoteConfig.RemoteConfig).to.be.deep.equal(RemoteConfigImpl); + }); + + 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('#appCheck()', () => { + it('should throw when called before initializing an app', () => { + expect(() => { + firebaseNamespace.appCheck(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should throw when default app is not initialized', () => { + firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + expect(() => { + firebaseNamespace.appCheck(); + }).to.throw(DEFAULT_APP_NOT_FOUND); + }); + + it('should return a valid namespace when the default app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions); + const fac: AppCheck = firebaseNamespace.appCheck(); + expect(fac.app).to.be.deep.equal(app); + }); + + it('should return a valid namespace when the named app is initialized', () => { + const app: App = firebaseNamespace.initializeApp(mocks.appOptions, 'testApp'); + const fac: AppCheck = firebaseNamespace.appCheck(app); + expect(fac.app).to.be.deep.equal(app); + }); + + it('should return a reference to AppCheck type', () => { + expect(firebaseNamespace.appCheck.AppCheck).to.be.deep.equal(AppCheckImpl); + }); + + it('should return a cached version of AppCheck on subsequent calls', () => { + firebaseNamespace.initializeApp(mocks.appOptions); + const service1: AppCheck = firebaseNamespace.appCheck(); + const service2: AppCheck = firebaseNamespace.appCheck(); + expect(service1).to.equal(service2); }); }); }); diff --git a/test/unit/firebase.spec.ts b/test/unit/firebase.spec.ts index eca361b7e4..da1256a2c2 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"); @@ -27,7 +28,10 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as mocks from '../resources/mocks'; import * as firebaseAdmin from '../../src/index'; -import {RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault} from '../../src/auth/credential'; +import { FirebaseApp, FirebaseAppInternals } from '../../src/firebase-app'; +import { + RefreshTokenCredential, ServiceAccountCredential, isApplicationDefault +} from '../../src/credential/credential-internal'; chai.should(); chai.use(chaiAsPromised); @@ -113,7 +117,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); @@ -124,7 +128,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); @@ -136,7 +140,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.true; - return firebaseAdmin.app().INTERNAL.getToken().then((token) => { + return getAppInternals().getToken().then((token) => { if (typeof credPath === 'undefined') { delete process.env.GOOGLE_APPLICATION_CREDENTIALS; } else { @@ -158,7 +162,7 @@ describe('Firebase', () => { }); expect(isApplicationDefault(firebaseAdmin.app().options.credential)).to.be.false; - return firebaseAdmin.app().INTERNAL.getToken() + return getAppInternals().getToken() .should.eventually.have.keys(['accessToken', 'expirationTime']); }); }); @@ -231,6 +235,21 @@ describe('Firebase', () => { }); }); + describe('#appCheck', () => { + it('should throw if the app has not been initialized', () => { + expect(() => { + return firebaseAdmin.appCheck(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should return the appCheck service', () => { + firebaseAdmin.initializeApp(mocks.appOptions); + expect(() => { + return firebaseAdmin.appCheck(); + }).not.to.throw(); + }); + }); + describe('#storage', () => { it('should throw if the app has not be initialized', () => { expect(() => { @@ -245,4 +264,8 @@ describe('Firebase', () => { }).not.to.throw(); }); }); + + function getAppInternals(): FirebaseAppInternals { + return (firebaseAdmin.app() as FirebaseApp).INTERNAL; + } }); diff --git a/test/unit/firestore/firestore.spec.ts b/test/unit/firestore/firestore.spec.ts index 482211ffa0..197e8dfde6 100644 --- a/test/unit/firestore/firestore.spec.ts +++ b/test/unit/firestore/firestore.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,12 +18,14 @@ '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 { ComputeEngineCredential, RefreshTokenCredential } from '../../../src/auth/credential'; -import {FirestoreService, getFirestoreOptions} from '../../../src/firestore/firestore'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { + ComputeEngineCredential, RefreshTokenCredential +} from '../../../src/credential/credential-internal'; +import { FirestoreService, getFirestoreOptions } from '../../../src/firestore/firestore-internal'; describe('Firestore', () => { let mockApp: FirebaseApp; diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts old mode 100755 new mode 100644 index 70583f73a6..1cccc1def5 --- 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"); @@ -24,10 +25,11 @@ 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/user-record.spec'; import './auth/token-generator.spec'; import './auth/token-verifier.spec'; @@ -38,6 +40,9 @@ import './auth/auth-config.spec'; import './auth/tenant.spec'; import './auth/tenant-manager.spec'; +// Credential +import './credential/credential.spec'; + // Database import './database/database.spec'; @@ -55,9 +60,12 @@ import './storage/storage.spec'; // Firestore import './firestore/firestore.spec'; +// Installations +import './installations/installations.spec'; +import './installations/installations-request-handler.spec'; + // InstanceId import './instance-id/instance-id.spec'; -import './instance-id/instance-id-request.spec'; // ProjectManagement import './project-management/project-management.spec'; @@ -72,3 +80,9 @@ import './security-rules/security-rules-api-client.spec'; // RemoteConfig import './remote-config/remote-config.spec'; import './remote-config/remote-config-api-client.spec'; + +// AppCheck +import './app-check/app-check.spec'; +import './app-check/app-check-api-client-internal.spec'; +import './app-check/token-generator.spec'; +import './app-check/token-verifier.spec.ts'; diff --git a/test/unit/instance-id/instance-id-request.spec.ts b/test/unit/installations/installations-request-handler.spec.ts similarity index 68% rename from test/unit/instance-id/instance-id-request.spec.ts rename to test/unit/installations/installations-request-handler.spec.ts index 7515f9cd7e..dcc32b464b 100644 --- a/test/unit/instance-id/instance-id-request.spec.ts +++ b/test/unit/installations/installations-request-handler.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,9 +26,9 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; -import {HttpClient} from '../../../src/utils/api-request'; -import {FirebaseInstanceIdRequestHandler} from '../../../src/instance-id/instance-id-request'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; chai.should(); chai.use(sinonChai); @@ -35,7 +36,7 @@ chai.use(chaiAsPromised); const expect = chai.expect; -describe('FirebaseInstanceIdRequestHandler', () => { +describe('FirebaseInstallationsRequestHandler', () => { const projectId = 'project_id'; const mockAccessToken: string = utils.generateRandomAccessToken(); let stubs: sinon.SinonStub[] = []; @@ -68,24 +69,24 @@ describe('FirebaseInstanceIdRequestHandler', () => { describe('Constructor', () => { it('should succeed with a FirebaseApp instance', () => { expect(() => { - return new FirebaseInstanceIdRequestHandler(mockApp); + return new FirebaseInstallationsRequestHandler(mockApp); }).not.to.throw(Error); }); }); - describe('deleteInstanceId', () => { + describe('deleteInstallation', () => { const httpMethod = 'DELETE'; const host = 'console.firebase.google.com'; - const path = `/v1/project/${projectId}/instanceId/test-iid`; + const path = `/v1/project/${projectId}/instanceId/test-fid`; const timeout = 10000; - it('should be fulfilled given a valid instance ID', () => { + it('should be fulfilled given a valid installation ID', () => { const stub = sinon.stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom('')); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith({ method: httpMethod, @@ -101,14 +102,14 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); - expect(error.message).to.equal('Instance ID "test-iid": Failed to find the instance ID.'); + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Failed to find the installation ID.'); }); }); @@ -117,30 +118,30 @@ describe('FirebaseInstanceIdRequestHandler', () => { .rejects(utils.errorFrom({}, 409)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); - expect(error.message).to.equal('Instance ID "test-iid": Already deleted.'); + expect(error.code).to.equal('installations/api-error'); + expect(error.message).to.equal('Installation ID "test-fid": Already deleted.'); }); }); it('should throw for unexpected HTTP errors', () => { - const expectedResult = {error: 'test error'}; + const expectedResult = { error: 'test error' }; const stub = sinon.stub(HttpClient.prototype, 'send') .rejects(utils.errorFrom(expectedResult, 511)); stubs.push(stub); - const requestHandler = new FirebaseInstanceIdRequestHandler(mockApp); - return requestHandler.deleteInstanceId('test-iid') + const requestHandler = new FirebaseInstallationsRequestHandler(mockApp); + return requestHandler.deleteInstallation('test-fid') .then(() => { throw new Error('Unexpected success'); }) .catch((error) => { - expect(error.code).to.equal('instance-id/api-error'); + expect(error.code).to.equal('installations/api-error'); expect(error.message).to.equal('test error'); }); }); diff --git a/test/unit/installations/installations.spec.ts b/test/unit/installations/installations.spec.ts new file mode 100644 index 0000000000..e7816fa556 --- /dev/null +++ b/test/unit/installations/installations.spec.ts @@ -0,0 +1,190 @@ +/*! + * @license + * Copyright 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; + +import { Installations } from '../../../src/installations/installations'; +import { FirebaseInstallationsRequestHandler } from '../../../src/installations/installations-request-handler'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { FirebaseInstallationsError, InstallationsClientErrorCode } from '../../../src/utils/error'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Installations', () => { + let fis: Installations; + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; + + let nullAccessTokenClient: Installations; + let malformedAccessTokenClient: Installations; + let rejectedPromiseAccessTokenClient: Installations; + + let googleCloudProject: string | undefined; + let gcloudProject: string | undefined; + + const noProjectIdError = 'Failed to determine project ID for Installations. Initialize the SDK ' + + 'with service account credentials or set project ID as an app option. Alternatively set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(undefined, mockApp); + mockCredentialApp = mocks.mockCredentialApp(); + fis = new Installations(mockApp); + + googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT; + gcloudProject = process.env.GCLOUD_PROJECT; + + nullAccessTokenClient = new Installations(mocks.appReturningNullAccessToken()); + malformedAccessTokenClient = new Installations(mocks.appReturningMalformedAccessToken()); + rejectedPromiseAccessTokenClient = new Installations(mocks.appRejectedWhileFetchingAccessToken()); + }); + + afterEach(() => { + getTokenStub.restore(); + process.env.GOOGLE_CLOUD_PROJECT = googleCloudProject; + process.env.GCLOUD_PROJECT = gcloudProject; + return mockApp.delete(); + }); + + + describe('Constructor', () => { + const invalidApps = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop]; + invalidApps.forEach((invalidApp) => { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const iidAny: any = Installations; + return new iidAny(invalidApp); + }).to.throw('First argument passed to admin.installations() must be a valid Firebase app instance.'); + }); + }); + + it('should throw given no app', () => { + expect(() => { + const iidAny: any = Installations; + return new iidAny(); + }).to.throw('First argument passed to admin.installations() must be a valid Firebase app instance.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const installations = new Installations(mockCredentialApp); + return installations.deleteInstallation('iid') + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Installations(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(fis.app).to.equal(mockApp); + }); + + it('is read-only', () => { + expect(() => { + (fis as any).app = mockApp; + }).to.throw('Cannot set property app of # which has only a getter'); + }); + }); + + describe('deleteInstallation()', () => { + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + const expectedError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const testInstallationId = 'test-iid'; + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + it('should be rejected given no installation ID', () => { + return (fis as any).deleteInstallation() + .should.eventually.be.rejected.and.have.property('code', 'installations/invalid-installation-id'); + }); + + it('should be rejected given an invalid installation ID', () => { + return fis.deleteInstallation('') + .should.eventually.be.rejected.and.have.property('code', 'installations/invalid-installation-id'); + }); + + it('should be rejected given an app which returns null access tokens', () => { + return nullAccessTokenClient.deleteInstallation(testInstallationId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which returns invalid access tokens', () => { + return malformedAccessTokenClient.deleteInstallation(testInstallationId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should be rejected given an app which fails to generate access tokens', () => { + return rejectedPromiseAccessTokenClient.deleteInstallation(testInstallationId) + .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); + }); + + it('should resolve without errors on success', () => { + const stub = sinon.stub(FirebaseInstallationsRequestHandler.prototype, 'deleteInstallation') + .resolves(); + stubs.push(stub); + return fis.deleteInstallation(testInstallationId) + .then(() => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(testInstallationId); + }); + }); + + it('should throw an error when the backend returns an error', () => { + // Stub deleteInstallation to throw a backend error. + const stub = sinon.stub(FirebaseInstallationsRequestHandler.prototype, 'deleteInstallation') + .rejects(expectedError); + stubs.push(stub); + return fis.deleteInstallation(testInstallationId) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Confirm underlying API called with expected parameters. + expect(stub).to.have.been.calledOnce.and.calledWith(testInstallationId); + // Confirm expected error returned. + expect(error).to.equal(expectedError); + }); + }); + }); +}); diff --git a/test/unit/instance-id/instance-id.spec.ts b/test/unit/instance-id/instance-id.spec.ts index db4a9dcb10..a35d2a01e2 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"); @@ -25,10 +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 { InstanceId } from '../../../src/instance-id/instance-id'; +import { Installations } from '../../../src/installations/installations'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { + FirebaseInstallationsError, FirebaseInstanceIdError, + InstallationsClientErrorCode, InstanceIdClientErrorCode, +} from '../../../src/utils/error'; chai.should(); chai.use(sinonChai); @@ -49,7 +53,7 @@ describe('InstanceId', () => { let googleCloudProject: string | undefined; let gcloudProject: string | undefined; - const noProjectIdError = 'Failed to determine project ID for InstanceId. Initialize the SDK ' + const noProjectIdError = 'Failed to determine project ID for Installations. Initialize the SDK ' + 'with service account credentials or set project ID as an app option. Alternatively set the ' + 'GOOGLE_CLOUD_PROJECT environment variable.'; @@ -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,8 +163,8 @@ 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(() => { @@ -170,10 +173,11 @@ describe('InstanceId', () => { }); }); - it('should throw an error when the backend returns an error', () => { + it('should throw a FirebaseInstanceIdError error when the backend returns an error', () => { // Stub deleteInstanceId to throw a backend error. - const stub = sinon.stub(FirebaseInstanceIdRequestHandler.prototype, 'deleteInstanceId') - .returns(Promise.reject(expectedError)); + const originalError = new FirebaseInstallationsError(InstallationsClientErrorCode.API_ERROR); + const stub = sinon.stub(Installations.prototype, 'deleteInstallation') + .rejects(originalError); stubs.push(stub); return iid.deleteInstanceId(testInstanceId) .then(() => { @@ -182,7 +186,9 @@ describe('InstanceId', () => { // Confirm underlying API called with expected parameters. expect(stub).to.have.been.calledOnce.and.calledWith(testInstanceId); // Confirm expected error returned. - expect(error).to.equal(expectedError); + const expectedError = new FirebaseInstanceIdError(InstanceIdClientErrorCode.API_ERROR); + expect(error).to.be.instanceOf(FirebaseInstanceIdError) + expect(error).to.deep.include(expectedError); }); }); }); diff --git a/test/unit/machine-learning/machine-learning-api-client.spec.ts b/test/unit/machine-learning/machine-learning-api-client.spec.ts index b308164a8b..3399dd0f52 100644 --- a/test/unit/machine-learning/machine-learning-api-client.spec.ts +++ b/test/unit/machine-learning/machine-learning-api-client.spec.ts @@ -19,14 +19,18 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { MachineLearningApiClient, ModelContent, - ListModelsOptions } from '../../../src/machine-learning/machine-learning-api-client'; import { 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/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; +import { MachineLearningApiClient } from '../../../src/machine-learning/machine-learning-api-client'; +import { machineLearning } from '../../../src/machine-learning/index'; + +import ListModelsOptions = machineLearning.ListModelsOptions; +import ModelOptions = machineLearning.ModelOptions; const expect = chai.expect; @@ -43,7 +47,7 @@ describe('MachineLearningApiClient', () => { modelHash: 'modelHash123', displayName: 'model_1', tags: ['tag_1', 'tag_2'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', sizeBytes: 16900988, @@ -57,16 +61,35 @@ describe('MachineLearningApiClient', () => { modelHash: 'modelHash234', displayName: 'model_2', tags: ['tag_2', 'tag_3'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', sizeBytes: 2220022, }, }; + const MODEL_RESPONSE_AUTOML = { + name: 'projects/test-project/models/3456789', + createTime: '2020-07-15T18:12:25.123987Z', + updateTime: '2020-07-15T19:15:32.965435Z', + etag: 'etag345', + modelHash: 'modelHash345', + displayName: 'model_automl', + tags: ['tag_automl'], + state: { published: true }, + tfliteModel: { + automlModel: 'projects/65432/models/ICN123', + sizeBytes: 3330033, + }, + }; + const PROJECT_ID = 'test-project'; + const PROJECT_NUMBER = '1234567'; + const OPERATION_ID = '987654'; + const OPERATION_NAME = `projects/${PROJECT_NUMBER}/operations/${OPERATION_ID}`; + const STATUS_ERROR_MESSAGE = 'Invalid Argument message' const STATUS_ERROR_RESPONSE = { code: 3, - message: 'Invalid Argument message', + message: STATUS_ERROR_MESSAGE, }; const OPERATION_SUCCESS_RESPONSE = { done: true, @@ -76,6 +99,34 @@ describe('MachineLearningApiClient', () => { done: true, error: STATUS_ERROR_RESPONSE, }; + const OPERATION_NOT_DONE_RESPONSE = { + name: OPERATION_NAME, + metadata: { + '@type': 'type.googleapis.com/google.firebase.ml.v1beta2.ModelOperationMetadata', + name: `projects/${PROJECT_ID}/models/${MODEL_ID}`, + basicOperationStatus: 'BASIC_OPERATION_STATUS_UPLOADING' + }, + done: false, + }; + const OPERATION_AUTOML_RESPONSE = { + done: true, + response: MODEL_RESPONSE_AUTOML, + }; + const LOCKED_MODEL_RESPONSE = { + name: 'projects/test-project/models/1234567', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag123', + modelHash: 'modelHash123', + displayName: 'model_1', + tags: ['tag_1', 'tag_2'], + activeOperations: [OPERATION_NOT_DONE_RESPONSE], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; const ERROR_RESPONSE = { error: { @@ -86,7 +137,7 @@ describe('MachineLearningApiClient', () => { }; const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + 'account credentials, or set project ID as an app option. Alternatively, set the ' @@ -124,10 +175,21 @@ describe('MachineLearningApiClient', () => { }); describe('createModel', () => { - const NAME_ONLY_CONTENT: ModelContent = {displayName: 'name1'}; - + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + const AUTOML_OPTIONS: ModelOptions = { + displayName: 'name3', + tfliteModel: { + automlModel: 'automlModel', + }, + }; - const invalidContent: any[] = [null, undefined, {}, { tags: []}]; + const invalidContent: any[] = [null, undefined, {}, { tags: [] }]; invalidContent.forEach((content) => { it(`should reject when called with: ${JSON.stringify(content)}`, () => { return apiClient.createModel(content) @@ -137,7 +199,7 @@ describe('MachineLearningApiClient', () => { }); it('should reject when project id is not available', () => { - return clientWithoutProjectId.createModel(NAME_ONLY_CONTENT) + return clientWithoutProjectId.createModel(NAME_ONLY_OPTIONS) .should.eventually.be.rejectedWith(noProjectId); }); @@ -147,8 +209,8 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); - return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + return apiClient.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); }); it('should resolve with the created resource on success', () => { @@ -156,23 +218,49 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + 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 accept AutoML options', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_AUTOML_RESPONSE)); + stubs.push(stub); + return apiClient.createModel(AUTOML_OPTIONS) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE_AUTOML); + }); + }); + it('should resolve with error when the operation fails', () => { const stub = sinon .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) + return apiClient.createModel(NAME_ONLY_OPTIONS) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); }); }); @@ -183,8 +271,8 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); - return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + return apiClient.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -194,8 +282,8 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + return apiClient.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with when failed with a FirebaseAppError', () => { @@ -204,19 +292,38 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(expected); stubs.push(stub); - return apiClient.createModel(NAME_ONLY_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + return apiClient.createModel(NAME_ONLY_OPTIONS) + .should.eventually.be.rejected.and.deep.include(expected); }); }); describe('updateModel', () => { - const NAME_ONLY_CONTENT: ModelContent = {displayName: 'name1'}; - const NAME_ONLY_MASK = ['displayName']; + const NAME_ONLY_OPTIONS: ModelOptions = { displayName: 'name1' }; + const GCS_OPTIONS: ModelOptions = { + displayName: 'name2', + tfliteModel: { + gcsTfliteUri: 'gcsUri1', + }, + }; + const AUTOML_OPTIONS: ModelOptions = { + displayName: 'name3', + tfliteModel: { + automlModel: 'automlModel', + }, + }; - const invalidContent: any[] = [null, undefined]; - invalidContent.forEach((content) => { - it(`should reject when called with: ${JSON.stringify(content)}`, () => { - return apiClient.updateModel(MODEL_ID, content, NAME_ONLY_MASK) + const NAME_ONLY_MASK_LIST = ['displayName']; + const GCS_MASK_LIST = ['displayName', 'tfliteModel.gcsTfliteUri']; + const AUTOML_MASK_LIST = ['displayName', 'tfliteModel.automlModel']; + + const NAME_ONLY_UPDATE_MASK_STRING = 'updateMask=displayName'; + const GCS_UPDATE_MASK_STRING = 'updateMask=displayName,tfliteModel.gcsTfliteUri'; + const AUTOML_UPDATE_MASK_STRING = 'updateMask=displayName,tfliteModel.automlModel'; + + const invalidOptions: any[] = [null, undefined]; + invalidOptions.forEach((option) => { + it(`should reject when called with: ${JSON.stringify(option)}`, () => { + return apiClient.updateModel(MODEL_ID, option, NAME_ONLY_MASK_LIST) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid model or mask content.'); }); @@ -229,7 +336,7 @@ describe('MachineLearningApiClient', () => { }); it('should reject when project id is not available', () => { - return clientWithoutProjectId.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return clientWithoutProjectId.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .should.eventually.be.rejectedWith(noProjectId); }); @@ -239,8 +346,8 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + 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', () => { @@ -248,16 +355,54 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.response).to.deep.equal(MODEL_RESPONSE); expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'PATCH', headers: EXPECTED_HEADERS, - url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?updateMask=displayName`, - data: NAME_ONLY_CONTENT, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${NAME_ONLY_UPDATE_MASK_STRING}`, + data: NAME_ONLY_OPTIONS, + }); + }); + }); + + it('should resolve with the updated GCS resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.updateModel(MODEL_ID, GCS_OPTIONS, GCS_MASK_LIST) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + headers: EXPECTED_HEADERS, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${GCS_UPDATE_MASK_STRING}`, + data: GCS_OPTIONS, + }); + }); + }); + + it('should resolve with the updated AutoML resource on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(OPERATION_SUCCESS_RESPONSE)); + stubs.push(stub); + return apiClient.updateModel(MODEL_ID, AUTOML_OPTIONS, AUTOML_MASK_LIST) + .then((resp) => { + expect(resp.done).to.be.true; + expect(resp.name).to.be.undefined; + expect(resp.response).to.deep.equal(MODEL_RESPONSE); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PATCH', + headers: EXPECTED_HEADERS, + url: `${BASE_URL}/projects/test-project/models/${MODEL_ID}?${AUTOML_UPDATE_MASK_STRING}`, + data: AUTOML_OPTIONS, }); }); }); @@ -267,10 +412,10 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .resolves(utils.responseFrom(OPERATION_ERROR_RESPONSE)); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) .then((resp) => { expect(resp.done).to.be.true; - expect(resp.name).to.be.empty; + expect(resp.name).to.be.undefined; expect(resp.error).to.deep.equal(STATUS_ERROR_RESPONSE); }); }); @@ -281,8 +426,8 @@ describe('MachineLearningApiClient', () => { .rejects(utils.errorFrom({}, 404)); stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + 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', () => { @@ -292,8 +437,8 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + 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', () => { @@ -302,8 +447,8 @@ describe('MachineLearningApiClient', () => { .stub(HttpClient.prototype, 'send') .rejects(expected); stubs.push(stub); - return apiClient.updateModel(MODEL_ID, NAME_ONLY_CONTENT, NAME_ONLY_MASK) - .should.eventually.be.rejected.and.deep.equal(expected); + return apiClient.updateModel(MODEL_ID, NAME_ONLY_OPTIONS, NAME_ONLY_MASK_LIST) + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -317,13 +462,13 @@ describe('MachineLearningApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.getModel('projects/foo/models/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Model ID must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getModel(MODEL_ID) .should.eventually.be.rejectedWith(noProjectId); }); @@ -351,7 +496,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject unknown-error when error code is not present', () => { @@ -361,7 +506,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject unknown-error for non-json response', () => { @@ -372,7 +517,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when failed with a FirebaseAppError', () => { @@ -382,10 +527,157 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); + 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], @@ -395,7 +687,7 @@ describe('MachineLearningApiClient', () => { const invalidListFilters: any[] = [null, 0, '', true, {}, []]; invalidListFilters.forEach((invalidFilter) => { it(`should reject when called with invalid pageToken: ${JSON.stringify(invalidFilter)}`, () => { - return apiClient.listModels({filter: invalidFilter}) + return apiClient.listModels({ filter: invalidFilter }) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid list filter.'); }); @@ -404,7 +696,7 @@ describe('MachineLearningApiClient', () => { const invalidPageSizes: any[] = [null, '', '10', true, {}, []]; invalidPageSizes.forEach((invalidPageSize) => { it(`should reject when called with invalid page size: ${JSON.stringify(invalidPageSize)}`, () => { - return apiClient.listModels({pageSize: invalidPageSize}) + return apiClient.listModels({ pageSize: invalidPageSize }) .should.eventually.be.rejected.and.have.property( 'message', 'Invalid page size.'); }); @@ -413,7 +705,7 @@ describe('MachineLearningApiClient', () => { const outOfRangePageSizes: number[] = [-1, 0, 101]; outOfRangePageSizes.forEach((invalidPageSize) => { it(`should reject when called with invalid page size: ${invalidPageSize}`, () => { - return apiClient.listModels({pageSize: invalidPageSize}) + return apiClient.listModels({ pageSize: invalidPageSize }) .should.eventually.be.rejected.and.have.property( 'message', 'Page size must be between 1 and 100.'); }); @@ -422,7 +714,7 @@ describe('MachineLearningApiClient', () => { const invalidPageTokens: any[] = [null, 0, '', true, {}, []]; invalidPageTokens.forEach((invalidToken) => { it(`should reject when called with invalid pageToken: ${JSON.stringify(invalidToken)}`, () => { - return apiClient.listModels({pageToken: invalidToken}) + return apiClient.listModels({ pageToken: invalidToken }) .should.eventually.be.rejected.and.have.property( 'message', 'Next page token must be a non-empty string.'); }); @@ -446,9 +738,9 @@ describe('MachineLearningApiClient', () => { }); const validOptions: ListModelsOptions[] = [ - {pageSize: 5}, - {pageToken: 'next'}, - {filter: 'displayName=name1'}, + { pageSize: 5 }, + { pageToken: 'next' }, + { filter: 'displayName=name1' }, { filter: 'displayName=name1', pageSize: 5, @@ -484,7 +776,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -494,7 +786,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -505,7 +797,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -515,7 +807,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.listModels() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -529,13 +821,13 @@ describe('MachineLearningApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.deleteModel('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Model ID must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.deleteModel(MODEL_ID) .should.eventually.be.rejectedWith(noProjectId); }); @@ -562,7 +854,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('not-found', 'Requested entity not found'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error when error code is not present', () => { @@ -572,7 +864,7 @@ describe('MachineLearningApiClient', () => { stubs.push(stub); const expected = new FirebaseMachineLearningError('unknown-error', 'Unknown server error: {}'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -583,7 +875,7 @@ describe('MachineLearningApiClient', () => { const expected = new FirebaseMachineLearningError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when failed with a FirebaseAppError', () => { @@ -593,7 +885,7 @@ describe('MachineLearningApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.deleteModel(MODEL_ID) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); diff --git a/test/unit/machine-learning/machine-learning.spec.ts b/test/unit/machine-learning/machine-learning.spec.ts index 1a49b2cab1..96493d298b 100644 --- a/test/unit/machine-learning/machine-learning.spec.ts +++ b/test/unit/machine-learning/machine-learning.spec.ts @@ -19,19 +19,30 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { MachineLearning, Model } from '../../../src/machine-learning/machine-learning'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; -import { MachineLearningApiClient, StatusErrorResponse, - ModelOptions, ModelResponse } from '../../../src/machine-learning/machine-learning-api-client'; +import { MachineLearning, Model } from '../../../src/machine-learning/machine-learning'; +import { + MachineLearningApiClient, + StatusErrorResponse, + ModelResponse, + OperationResponse +} from '../../../src/machine-learning/machine-learning-api-client'; import { FirebaseMachineLearningError } from '../../../src/machine-learning/machine-learning-utils'; import { deepCopy } from '../../../src/utils/deep-copy'; +import { machineLearning } from '../../../src/machine-learning/index'; + +import ModelOptions = machineLearning.ModelOptions; const expect = chai.expect; 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'; @@ -62,13 +73,13 @@ describe('MachineLearning', () => { modelHash: 'modelHash123', displayName: 'model_1', tags: ['tag_1', 'tag_2'], - state: {published: true}, + state: { published: true }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', sizeBytes: 16900988, }, }; - const MODEL1 = new Model(MODEL_RESPONSE); + const MODEL_RESPONSE2: { name: string; @@ -97,13 +108,27 @@ describe('MachineLearning', () => { modelHash: 'modelHash234', displayName: 'model_2', tags: ['tag_2', 'tag_3'], - state: {published: false}, + state: { published: false }, tfliteModel: { gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model2.tflite', sizeBytes: 22200222, }, }; - const MODEL2 = new Model(MODEL_RESPONSE2); + + const MODEL_RESPONSE3: any = { + name: 'projects/test-project/models/3456789', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag345', + modelHash: 'modelHash345', + displayName: 'model_3', + tags: ['tag_3', 'tag_4'], + state: { published: true }, + tfliteModel: { + managedUpload: true, + sizeBytes: 22200222, + }, + }; const STATUS_ERROR_RESPONSE: { code: number; @@ -115,6 +140,7 @@ describe('MachineLearning', () => { const OPERATION_RESPONSE: { name?: string; + metadata?: any; done: boolean; error?: StatusErrorResponse; response?: { @@ -144,6 +170,7 @@ describe('MachineLearning', () => { const OPERATION_RESPONSE_ERROR: { name?: string; + metadata?: any; done: boolean; error?: { code: number; @@ -155,18 +182,79 @@ describe('MachineLearning', () => { error: STATUS_ERROR_RESPONSE, }; + const OPERATION_RESPONSE_NOT_DONE: { + name?: string; + metadata?: any; + done: boolean; + error?: { + code: number; + message: string; + }; + response?: ModelResponse; + } = { + name: OPERATION_NAME, + metadata: { + '@type': 'type.googleapis.com/google.firebase.ml.v1beta2.ModelOperationMetadata', + name: `projects/${PROJECT_ID}/models/${MODEL_ID}`, + basicOperationStatus: 'BASIC_OPERATION_STATUS_UPLOADING' + }, + done: false, + }; + + const MODEL_RESPONSE_LOCKED: { + name: string; + createTime: string; + updateTime: string; + etag: string; + modelHash: string; + displayName?: string; + tags?: string[]; + activeOperations?: OperationResponse[]; + state?: { + validationError?: { + code: number; + message: string; + }; + published?: boolean; + }; + tfliteModel?: { + gcsTfliteUri: string; + sizeBytes: number; + }; + } = { + name: 'projects/test-project/models/1234567', + createTime: '2020-02-07T23:45:23.288047Z', + updateTime: '2020-02-08T23:45:23.288047Z', + etag: 'etag123', + modelHash: 'modelHash123', + displayName: 'model_1', + tags: ['tag_1', 'tag_2'], + activeOperations: [OPERATION_RESPONSE_NOT_DONE], + state: { published: true }, + tfliteModel: { + gcsTfliteUri: 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite', + sizeBytes: 16900988, + }, + }; let machineLearning: MachineLearning; let mockApp: FirebaseApp; + let mockClient: MachineLearningApiClient; let mockCredentialApp: FirebaseApp; + let model1: Model; + let model2: Model; + const stubs: sinon.SinonStub[] = []; before(() => { mockApp = mocks.app(); + mockClient = new MachineLearningApiClient(mockApp); mockCredentialApp = mocks.mockCredentialApp(); machineLearning = new MachineLearning(mockApp); + model1 = new Model(MODEL_RESPONSE, mockClient); + model2 = new Model(MODEL_RESPONSE2, mockClient); }); after(() => { @@ -209,7 +297,7 @@ describe('MachineLearning', () => { displayName: 'foo', tfliteModel: { gcsTfliteUri: 'gs://some-bucket/model.tflite', - }}); + } }); }).to.throw(expectedError); }); @@ -229,13 +317,13 @@ describe('MachineLearning', () => { describe('Model', () => { it('should successfully construct a model', () => { - const model = new Model(MODEL_RESPONSE); + const model = new Model(MODEL_RESPONSE, mockClient); expect(model.modelId).to.equal(MODEL_ID); expect(model.displayName).to.equal('model_1'); expect(model.tags).to.deep.equal(['tag_1', 'tag_2']); expect(model.createTime).to.equal(CREATE_TIME_UTC); expect(model.updateTime).to.equal(UPDATE_TIME_UTC); - expect(model.validationError).to.be.empty; + expect(model.validationError).to.be.undefined; expect(model.published).to.be.true; expect(model.etag).to.equal('etag123'); expect(model.modelHash).to.equal('modelHash123'); @@ -245,8 +333,71 @@ describe('MachineLearning', () => { 'gs://test-project-bucket/Firebase/ML/Models/model1.tflite'); expect(tflite.sizeBytes).to.be.equal(16900988); }); + + it('should accept unknown fields gracefully', () => { + const model = new Model(MODEL_RESPONSE3, 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 @@ -260,7 +411,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'getModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.getModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( @@ -335,7 +486,7 @@ describe('MachineLearning', () => { return machineLearning.getModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); }); @@ -362,7 +513,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'listModels') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.listModels() .should.eventually.be.rejected.and.have.property( @@ -377,8 +528,8 @@ describe('MachineLearning', () => { return machineLearning.listModels() .then((result) => { expect(result.models.length).equals(2); - expect(result.models[0]).to.deep.equal(MODEL1); - expect(result.models[1]).to.deep.equal(MODEL2); + expect(result.models[0]).to.deep.equal(model1); + expect(result.models[1]).to.deep.equal(model2); expect(result.pageToken).to.equal(LIST_MODELS_RESPONSE.nextPageToken); }); }); @@ -397,7 +548,7 @@ describe('MachineLearning', () => { it('should resolve on success', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'deleteModel') - .resolves({}); + .resolves(); stubs.push(stub); return machineLearning.deleteModel(MODEL_ID); @@ -430,7 +581,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'createModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) .should.eventually.be.rejected.and.have.property( @@ -505,7 +656,7 @@ describe('MachineLearning', () => { return machineLearning.createModel(MODEL_OPTIONS_WITH_GCS) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -547,7 +698,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) .should.eventually.be.rejected.and.have.property( @@ -622,7 +773,7 @@ describe('MachineLearning', () => { return machineLearning.updateModel(MODEL_ID, MODEL_OPTIONS_WITH_GCS) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -651,7 +802,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.publishModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( @@ -726,7 +877,7 @@ describe('MachineLearning', () => { return machineLearning.publishModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); @@ -755,7 +906,7 @@ describe('MachineLearning', () => { it('should reject when API response is invalid', () => { const stub = sinon .stub(MachineLearningApiClient.prototype, 'updateModel') - .resolves(null); + .resolves(null as any); stubs.push(stub); return machineLearning.unpublishModel(MODEL_ID) .should.eventually.be.rejected.and.have.property( @@ -830,7 +981,7 @@ describe('MachineLearning', () => { return machineLearning.unpublishModel(MODEL_ID) .then((model) => { - expect(model).to.deep.equal(MODEL1); + expect(model).to.deep.equal(model1); }); }); diff --git a/test/unit/messaging/batch-requests.spec.ts b/test/unit/messaging/batch-requests.spec.ts index bcfee5ef74..4303c5aedd 100644 --- a/test/unit/messaging/batch-requests.spec.ts +++ b/test/unit/messaging/batch-requests.spec.ts @@ -24,7 +24,7 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import { HttpClient, HttpResponse, HttpRequestConfig, HttpError } from '../../../src/utils/api-request'; -import { SubRequest, BatchRequestClient } from '../../../src/messaging/batch-request'; +import { SubRequest, BatchRequestClient } from '../../../src/messaging/batch-request-internal'; chai.should(); chai.use(sinonChai); @@ -51,21 +51,21 @@ function createMultipartResponse(success: object[], failures: object[] = []): Ht const multipart: Buffer[] = []; success.forEach((part) => { let payload = ''; - payload += `HTTP/1.1 200 OK\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 200 OK\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; multipart.push(Buffer.from(payload, 'utf-8')); }); failures.forEach((part) => { let payload = ''; - payload += `HTTP/1.1 500 Internal Server Error\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 500 Internal Server Error\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; multipart.push(Buffer.from(payload, 'utf-8')); }); return { status: 200, - headers: {'Content-Type': 'multipart/mixed; boundary=boundary'}, + headers: { 'Content-Type': 'multipart/mixed; boundary=boundary' }, multipart, text: '', data: null, @@ -93,7 +93,7 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, + { url: 'https://example.com', body: { foo: 1 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -110,9 +110,9 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject, responseObject, responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -131,9 +131,9 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject, responseObject], [responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -150,12 +150,12 @@ describe('BatchRequestClient', () => { it('should reject on top-level HTTP error responses', async () => { const stub = sinon.stub(httpClient, 'send').rejects( - utils.errorFrom({error: 'test'})); + utils.errorFrom({ error: 'test' })); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, - {url: 'https://example.com', body: {foo: 3}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, + { url: 'https://example.com', body: { foo: 3 } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -174,10 +174,10 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}}, - {url: 'https://example.com', body: {foo: 2}}, + { url: 'https://example.com', body: { foo: 1 } }, + { url: 'https://example.com', body: { foo: 2 } }, ]; - const commonHeaders = {'X-Custom-Header': 'value'}; + const commonHeaders = { 'X-Custom-Header': 'value' }; const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); const responses: HttpResponse[] = await batch.send(requests); @@ -200,8 +200,8 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'value'}}, - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'value'}}, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'value' } }, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'value' } }, ]; const batch = new BatchRequestClient(httpClient, batchUrl); @@ -223,10 +223,10 @@ describe('BatchRequestClient', () => { createMultipartResponse([responseObject])); stubs.push(stub); const requests: SubRequest[] = [ - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'overwrite'}}, - {url: 'https://example.com', body: {foo: 1}, headers: {'X-Custom-Header': 'overwrite'}}, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'overwrite' } }, + { url: 'https://example.com', body: { foo: 1 }, headers: { 'X-Custom-Header': 'overwrite' } }, ]; - const commonHeaders = {'X-Custom-Header': 'value'}; + const commonHeaders = { 'X-Custom-Header': 'value' }; const batch = new BatchRequestClient(httpClient, batchUrl, commonHeaders); const responses: HttpResponse[] = await batch.send(requests); diff --git a/test/unit/messaging/messaging.spec.ts b/test/unit/messaging/messaging.spec.ts index 9bd41b374e..236c18213a 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"); @@ -26,15 +27,12 @@ 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 { - Message, MessagingOptions, MessagingPayload, MessagingDevicesResponse, MessagingDeviceGroupResponse, - MessagingTopicManagementResponse, BatchResponse, SendResponse, MulticastMessage, -} from '../../../src/messaging/messaging-types'; -import { - Messaging, BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS, -} from '../../../src/messaging/messaging'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { messaging } from '../../../src/messaging/index'; +import { Messaging } from '../../../src/messaging/messaging'; +import { BLACKLISTED_OPTIONS_KEYS, BLACKLISTED_DATA_PAYLOAD_KEYS } from '../../../src/messaging/messaging-internal'; import { HttpClient } from '../../../src/utils/api-request'; +import { getSdkVersion } from '../../../src/utils/index'; chai.should(); chai.use(sinonChai); @@ -42,6 +40,19 @@ chai.use(chaiAsPromised); const expect = chai.expect; +import Message = messaging.Message; +import TokenMessage = messaging.TokenMessage; +import TopicMessage = messaging.TopicMessage; +import ConditionMessage = messaging.ConditionMessage; +import MessagingOptions = messaging.MessagingOptions; +import MessagingPayload = messaging.MessagingPayload; +import MessagingDevicesResponse = messaging.MessagingDevicesResponse; +import MessagingDeviceGroupResponse = messaging.MessagingDeviceGroupResponse +import MessagingTopicManagementResponse = messaging.MessagingTopicManagementResponse; +import BatchResponse = messaging.BatchResponse; +import SendResponse = messaging.SendResponse; +import MulticastMessage = messaging.MulticastMessage; + // FCM endpoints const FCM_SEND_HOST = 'fcm.googleapis.com'; const FCM_SEND_PATH = '/fcm/send'; @@ -86,7 +97,7 @@ function mockBatchRequest(ids: string[]): nock.Scope { function mockBatchRequestWithErrors(ids: string[], errors: object[] = []): nock.Scope { const mockPayload = createMultipartPayloadWithErrors(ids.map((id) => { - return {name: id}; + return { name: id }; }), errors); return nock(`https://${FCM_SEND_HOST}:443`) .post('/batch') @@ -103,15 +114,15 @@ function createMultipartPayloadWithErrors( success.forEach((part) => { payload += `--${boundary}\r\n`; payload += 'Content-type: application/http\r\n\r\n'; - payload += `HTTP/1.1 200 OK\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 200 OK\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; }); failures.forEach((part) => { payload += `--${boundary}\r\n`; payload += 'Content-type: application/http\r\n\r\n'; - payload += `HTTP/1.1 500 Internal Server Error\r\n`; - payload += `Content-type: application/json\r\n\r\n`; + payload += 'HTTP/1.1 500 Internal Server Error\r\n'; + payload += 'Content-type: application/json\r\n\r\n'; payload += `${JSON.stringify(part)}\r\n`; }); payload += `--${boundary}--\r\n`; @@ -321,7 +332,7 @@ describe('Messaging', () => { const mockAccessToken: string = utils.generateRandomAccessToken(); const expectedHeaders = { 'Authorization': 'Bearer ' + mockAccessToken, - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'access_token_auth': 'true', }; const emptyResponse = utils.responseFrom({}); @@ -371,7 +382,7 @@ describe('Messaging', () => { it('should reject given app without project ID', () => { const appWithoutProjectId = mocks.mockCredentialApp(); const messagingWithoutProjectId = new Messaging(appWithoutProjectId); - messagingWithoutProjectId.send({topic: 'test'}) + messagingWithoutProjectId.send({ topic: 'test' }) .should.eventually.be.rejectedWith( 'Failed to determine project ID for Messaging. Initialize the SDK with service ' + 'account credentials or set project ID as an app option. Alternatively set the ' @@ -409,7 +420,7 @@ describe('Messaging', () => { }); const noTarget = [ - {}, {token: null}, {token: ''}, {topic: null}, {topic: ''}, {condition: null}, {condition: ''}, + {}, { token: null }, { token: '' }, { topic: null }, { topic: '' }, { condition: null }, { condition: '' }, ]; noTarget.forEach((message) => { it(`should throw given message without target: ${ JSON.stringify(message) }`, () => { @@ -420,10 +431,10 @@ describe('Messaging', () => { }); const multipleTargets = [ - {token: 'a', topic: 'b'}, - {token: 'a', condition: 'b'}, - {condition: 'a', topic: 'b'}, - {token: 'a', topic: 'b', condition: 'c'}, + { token: 'a', topic: 'b' }, + { token: 'a', condition: 'b' }, + { condition: 'a', topic: 'b' }, + { token: 'a', topic: 'b', condition: 'c' }, ]; multipleTargets.forEach((message) => { it(`should throw given message without target: ${ JSON.stringify(message)}`, () => { @@ -437,7 +448,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.send({token: 'a'}, dryRun as any); + messaging.send({ token: 'a' }, dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -446,14 +457,14 @@ describe('Messaging', () => { invalidTopics.forEach((topic) => { it(`should throw given invalid topic name: ${JSON.stringify(topic)}`, () => { expect(() => { - messaging.send({topic}); + messaging.send({ topic }); }).to.throw('Malformed topic name'); }); }); const targetMessages = [ - {token: 'mock-token'}, {topic: 'mock-topic'}, - {topic: '/topics/mock-topic'}, {condition: '"foo" in topics'}, + { token: 'mock-token' }, { topic: 'mock-topic' }, + { topic: '/topics/mock-topic' }, { condition: '"foo" in topics' }, ]; targetMessages.forEach((message) => { it(`should be fulfilled with a message ID given a valid message: ${JSON.stringify(message)}`, () => { @@ -482,7 +493,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(400, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/invalid-argument'); }); @@ -502,7 +513,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); @@ -523,7 +534,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/third-party-auth-error'); }); @@ -538,16 +549,16 @@ describe('Messaging', () => { }; mockedRequests.push(mockSendError(404, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockSendError(400, 'json', resp)); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); }); @@ -555,13 +566,13 @@ describe('Messaging', () => { // Error code will be determined based on the status code. mockedRequests.push(mockSendError(400, 'text', 'foo bar')); return messaging.send( - {token: 'mock-token'}, + { token: 'mock-token' }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); }); describe('sendAll()', () => { - const validMessage: Message = {token: 'a'}; + const validMessage: Message = { token: 'a' }; function checkSendResponseSuccess(response: SendResponse, messageId: string): void { expect(response.success).to.be.true; @@ -610,7 +621,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.sendAll([{token: 'a'}], dryRun as any); + messaging.sendAll([{ token: 'a' }], dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -795,7 +806,7 @@ describe('Messaging', () => { }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendAll( [validMessage], @@ -815,6 +826,32 @@ describe('Messaging', () => { [validMessage], ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); + + // This test was added to also verify https://github.com/firebase/firebase-admin-node/issues/1146 + it('should be fulfilled when called with different message types', () => { + const messageIds = [ + 'projects/projec_id/messages/1', + 'projects/projec_id/messages/2', + 'projects/projec_id/messages/3', + ]; + const tokenMessage: TokenMessage = { token: 'test' }; + const topicMessage: TopicMessage = { topic: 'test' }; + const conditionMessage: ConditionMessage = { condition: 'test' }; + const messages: Message[] = [tokenMessage, topicMessage, conditionMessage]; + + mockedRequests.push(mockBatchRequest(messageIds)); + + return messaging.sendAll(messages) + .then((response: BatchResponse) => { + expect(response.successCount).to.equal(3); + expect(response.failureCount).to.equal(0); + response.responses.forEach((resp, idx) => { + expect(resp.success).to.be.true; + expect(resp.messageId).to.equal(messageIds[idx]); + expect(resp.error).to.be.undefined; + }); + }); + }); }); describe('sendMulticast()', () => { @@ -822,9 +859,9 @@ describe('Messaging', () => { successCount: 3, failureCount: 0, responses: [ - {success: true, messageId: 'projects/projec_id/messages/1'}, - {success: true, messageId: 'projects/projec_id/messages/2'}, - {success: true, messageId: 'projects/projec_id/messages/3'}, + { success: true, messageId: 'projects/projec_id/messages/1' }, + { success: true, messageId: 'projects/projec_id/messages/2' }, + { success: true, messageId: 'projects/projec_id/messages/3' }, ], }; @@ -845,7 +882,7 @@ describe('Messaging', () => { messaging.sendMulticast({} as any); }).to.throw('tokens must be a non-empty array'); expect(() => { - messaging.sendMulticast({tokens: []}); + messaging.sendMulticast({ tokens: [] }); }).to.throw('tokens must be a non-empty array'); }); @@ -855,7 +892,7 @@ describe('Messaging', () => { tokens.push(`token${i}`); } expect(() => { - messaging.sendMulticast({tokens}); + messaging.sendMulticast({ tokens }); }).to.throw('tokens list must not contain more than 500 items'); }); @@ -863,7 +900,7 @@ describe('Messaging', () => { invalidDryRun.forEach((dryRun) => { it(`should throw given invalid dryRun parameter: ${JSON.stringify(dryRun)}`, () => { expect(() => { - messaging.sendMulticast({tokens: ['a']}, dryRun as any); + messaging.sendMulticast({ tokens: ['a'] }, dryRun as any); }).to.throw('dryRun must be a boolean'); }); }); @@ -871,7 +908,7 @@ describe('Messaging', () => { it('should create multiple messages using the empty multicast payload', () => { stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); const tokens = ['a', 'b', 'c']; - return messaging.sendMulticast({tokens}) + return messaging.sendMulticast({ tokens }) .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; @@ -879,7 +916,7 @@ describe('Messaging', () => { expect(messages.length).to.equal(3); expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { - expect((message as any).token).to.equal(tokens[idx]); + expect((message as TokenMessage).token).to.equal(tokens[idx]); expect(message.android).to.be.undefined; expect(message.apns).to.be.undefined; expect(message.data).to.be.undefined; @@ -894,12 +931,12 @@ describe('Messaging', () => { const tokens = ['a', 'b', 'c']; const multicast: MulticastMessage = { tokens, - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, - fcmOptions: {analyticsLabel: 'label'}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, + fcmOptions: { analyticsLabel: 'label' }, }; return messaging.sendMulticast(multicast) .then((response: BatchResponse) => { @@ -909,7 +946,7 @@ describe('Messaging', () => { expect(messages.length).to.equal(3); expect(stub!.args[0][1]).to.be.undefined; messages.forEach((message, idx) => { - expect((message as any).token).to.equal(tokens[idx]); + expect((message as TokenMessage).token).to.equal(tokens[idx]); expect(message.android).to.deep.equal(multicast.android); expect(message.apns).to.be.deep.equal(multicast.apns); expect(message.data).to.be.deep.equal(multicast.data); @@ -923,7 +960,7 @@ describe('Messaging', () => { it('should pass dryRun argument through', () => { stub = sinon.stub(messaging, 'sendAll').resolves(mockResponse); const tokens = ['a', 'b', 'c']; - return messaging.sendMulticast({tokens}, true) + return messaging.sendMulticast({ tokens }, true) .then((response: BatchResponse) => { expect(response).to.deep.equal(mockResponse); expect(stub).to.have.been.calledOnce; @@ -940,11 +977,11 @@ describe('Messaging', () => { mockedRequests.push(mockBatchRequest(messageIds)); return messaging.sendMulticast({ tokens: ['a', 'b', 'c'], - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, }).then((response: BatchResponse) => { expect(response.successCount).to.equal(3); expect(response.failureCount).to.equal(0); @@ -965,11 +1002,11 @@ describe('Messaging', () => { mockedRequests.push(mockBatchRequest(messageIds)); return messaging.sendMulticast({ tokens: ['a', 'b', 'c'], - android: {ttl: 100}, - apns: {payload: {aps: {badge: 42}}}, - data: {key: 'value'}, - notification: {title: 'test title'}, - webpush: {data: {webKey: 'webValue'}}, + android: { ttl: 100 }, + apns: { payload: { aps: { badge: 42 } } }, + data: { key: 'value' }, + notification: { title: 'test title' }, + webpush: { data: { webKey: 'webValue' } }, }, true).then((response: BatchResponse) => { expect(response.successCount).to.equal(3); expect(response.failureCount).to.equal(0); @@ -994,7 +1031,7 @@ describe('Messaging', () => { }, ]; mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); - return messaging.sendMulticast({tokens: ['a', 'b']}) + return messaging.sendMulticast({ tokens: ['a', 'b'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(2); expect(response.failureCount).to.equal(1); @@ -1027,7 +1064,7 @@ describe('Messaging', () => { }, ]; mockedRequests.push(mockBatchRequestWithErrors(messageIds, errors)); - return messaging.sendMulticast({tokens: ['a', 'b']}) + return messaging.sendMulticast({ tokens: ['a', 'b'] }) .then((response: BatchResponse) => { expect(response.successCount).to.equal(1); expect(response.failureCount).to.equal(1); @@ -1049,7 +1086,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/invalid-argument'); }); @@ -1069,7 +1106,7 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(404, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); @@ -1083,16 +1120,16 @@ describe('Messaging', () => { }; mockedRequests.push(mockBatchError(404, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejectedWith('test error message') .and.have.property('code', 'messaging/registration-token-not-registered'); }); it('should fail when the backend server returns an unknown error', () => { - const resp = {error: 'test error message'}; + const resp = { error: 'test error message' }; mockedRequests.push(mockBatchError(400, 'json', resp)); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/unknown-error'); }); @@ -1100,13 +1137,13 @@ describe('Messaging', () => { // Error code will be determined based on the status code. mockedRequests.push(mockBatchError(400, 'text', 'foo bar')); return messaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'messaging/invalid-argument'); }); it('should be rejected given an app which returns null access tokens', () => { return nullAccessTokenMessaging.sendMulticast( - {tokens: ['a']}, + { tokens: ['a'] }, ).should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); @@ -1417,6 +1454,9 @@ describe('Messaging', () => { mocks.messaging.registrationToken + '0', mocks.messaging.registrationToken + '1', ], + canonicalRegistrationTokenCount: -1, + multicastId: -1, + results: [], }); }); @@ -1667,6 +1707,7 @@ describe('Messaging', () => { results: [ { messageId: `0:${ mocks.messaging.messageId }` }, ], + failedRegistrationTokens: [], }); }); @@ -2438,7 +2479,7 @@ describe('Messaging', () => { }); }); - it(`should throw given an empty vibrateTimingsMillis array`, () => { + it('should throw given an empty vibrateTimingsMillis array', () => { const message: Message = { condition: 'topic-name', android: { @@ -2472,7 +2513,7 @@ describe('Messaging', () => { }); }); - it(`should throw given a negative light on duration`, () => { + it('should throw given a negative light on duration', () => { const message: Message = { condition: 'topic-name', android: { @@ -2491,7 +2532,7 @@ describe('Messaging', () => { 'android.notification.lightSettings.lightOnDurationMillis must be a non-negative duration in milliseconds'); }); - it(`should throw given a negative light off duration`, () => { + it('should throw given a negative light off duration', () => { const message: Message = { condition: 'topic-name', android: { @@ -2590,67 +2631,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'}); + messaging.send({ fcmOptions: arg, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); it(`should throw given invalid AndroidFcmOptions: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({android: {fcmOptions: arg}, topic: 'test'}); + messaging.send({ android: { fcmOptions: arg }, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); it(`should throw given invalid ApnsFcmOptions: ${JSON.stringify(arg)}`, () => { expect(() => { - messaging.send({apns: {fcmOptions: arg}, topic: 'test'}); + messaging.send({ apns: { fcmOptions: arg }, topic: 'test' }); }).to.throw('fcmOptions must be a non-null object'); }); }); invalidImages.forEach((imageUrl) => { - it(`should throw given invalid URL string for imageUrl`, () => { + it('should throw given invalid URL string for imageUrl', () => { expect(() => { - messaging.send({apns: {fcmOptions: {imageUrl}}, topic: 'test'}); + messaging.send({ apns: { fcmOptions: { imageUrl } }, topic: 'test' }); }).to.throw('imageUrl must be a valid URL string'); }); }); const invalidDataMessages: any[] = [ - {label: 'data', message: {data: {k1: true}}}, - {label: 'android.data', message: {android: {data: {k1: true}}}}, - {label: 'webpush.data', message: {webpush: {data: {k1: true}}}}, - {label: 'webpush.headers', message: {webpush: {headers: {k1: true}}}}, - {label: 'apns.headers', message: {apns: {headers: {k1: true}}}}, + { label: 'data', message: { data: { k1: true } } }, + { label: 'android.data', message: { android: { data: { k1: true } } } }, + { label: 'webpush.data', message: { webpush: { data: { k1: true } } } }, + { label: 'webpush.headers', message: { webpush: { headers: { k1: true } } } }, + { label: 'apns.headers', message: { apns: { headers: { k1: true } } } }, ]; invalidDataMessages.forEach((config) => { it(`should throw given data with non-string value: ${config.label}`, () => { @@ -2666,23 +2707,23 @@ describe('Messaging', () => { invalidApnsPayloads.forEach((payload) => { it(`should throw given APNS payload with invalid object: ${JSON.stringify(payload)}`, () => { expect(() => { - messaging.send({apns: {payload}, token: 'token'}); + messaging.send({ apns: { payload }, token: 'token' }); }).to.throw('apns.payload must be a non-null object'); }); }); invalidApnsPayloads.forEach((aps) => { it(`should throw given APNS payload with invalid aps object: ${JSON.stringify(aps)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps}}, token: 'token'}); + messaging.send({ apns: { payload: { aps } }, token: 'token' }); }).to.throw('apns.payload.aps must be a non-null object'); }); }); - it(`should throw given APNS payload with duplicate fields`, () => { + it('should throw given APNS payload with duplicate fields', () => { expect(() => { messaging.send({ apns: { payload: { - aps: {'mutableContent': true, 'mutable-content': 1}, + aps: { 'mutableContent': true, 'mutable-content': 1 }, }, }, token: 'token', @@ -2694,7 +2735,7 @@ describe('Messaging', () => { invalidApnsAlerts.forEach((alert) => { it(`should throw given APNS payload with invalid aps alert: ${JSON.stringify(alert)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps: {alert}}}, token: 'token'}); + messaging.send({ apns: { payload: { aps: { alert } } }, token: 'token' }); }).to.throw('apns.payload.aps.alert must be a string or a non-null object'); }); }); @@ -2703,7 +2744,7 @@ describe('Messaging', () => { invalidApnsSounds.forEach((sound) => { it(`should throw given APNS payload with invalid aps sound: ${JSON.stringify(sound)}`, () => { expect(() => { - messaging.send({apns: {payload: {aps: {sound}}}, token: 'token'}); + messaging.send({ apns: { payload: { aps: { sound } } }, token: 'token' }); }).to.throw('apns.payload.aps.sound must be a non-empty string or a non-null object'); }); }); @@ -2714,7 +2755,7 @@ describe('Messaging', () => { apns: { payload: { aps: { - sound: {name}, + sound: { name }, }, }, }, @@ -3283,7 +3324,7 @@ describe('Messaging', () => { threadId: 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -3320,7 +3361,7 @@ describe('Messaging', () => { 'thread-id': 'thread.id', }, customKey1: 'custom.value', - customKey2: {nested: 'value'}, + customKey2: { nested: 'value' }, }, fcmOptions: { analyticsLabel: 'test.analytics', @@ -3444,7 +3485,7 @@ describe('Messaging', () => { expectedReq: { apns: { payload: { - aps: {'content-available': 1}, + aps: { 'content-available': 1 }, }, }, }, @@ -3498,7 +3539,7 @@ describe('Messaging', () => { // Wait for the initial getToken() call to complete before stubbing https.request. return mockApp.INTERNAL.getToken() .then(() => { - const resp = utils.responseFrom({message: 'test'}); + const resp = utils.responseFrom({ message: 'test' }); httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); const req = config.req; req.token = 'mock-token'; @@ -3509,7 +3550,7 @@ describe('Messaging', () => { expectedReq.token = 'mock-token'; expect(httpsRequestStub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - data: {message: expectedReq}, + data: { message: expectedReq }, timeout: 10000, url: 'https://fcm.googleapis.com/v1/projects/project_id/messages:send', headers: expectedHeaders, @@ -3521,14 +3562,14 @@ describe('Messaging', () => { it('should not throw when the message is addressed to the prefixed topic name', () => { return mockApp.INTERNAL.getToken() .then(() => { - const resp = utils.responseFrom({message: 'test'}); + const resp = utils.responseFrom({ message: 'test' }); httpsRequestStub = sinon.stub(HttpClient.prototype, 'send').resolves(resp); - return messaging.send({topic: '/topics/mock-topic'}); + return messaging.send({ topic: '/topics/mock-topic' }); }) .then(() => { expect(httpsRequestStub).to.have.been.calledOnce; const requestData = httpsRequestStub.args[0][0].data; - const expectedReq = {topic: 'mock-topic'}; + const expectedReq = { topic: 'mock-topic' }; expect(requestData.message).to.deep.equal(expectedReq); }); }); @@ -3995,10 +4036,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 index d38c2600b7..5feff560a9 100644 --- a/test/unit/project-management/android-app.spec.ts +++ b/test/unit/project-management/android-app.spec.ts @@ -21,11 +21,16 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp, ShaCertificate } from '../../../src/project-management/android-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { AndroidAppMetadata, AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AndroidAppMetadata = projectManagement.AndroidAppMetadata; +import AppPlatform = projectManagement.AppPlatform; const expect = chai.expect; @@ -109,7 +114,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getResource') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getMetadata() .should.eventually.be.rejected @@ -200,7 +205,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getAndroidShaCertificates') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getShaCertificates() .should.eventually.be.rejected @@ -336,7 +341,7 @@ describe('AndroidApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getConfig') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return androidApp.getConfig() .should.eventually.be.rejected @@ -357,7 +362,7 @@ describe('AndroidApp', () => { .should.eventually.be.rejected .and.have.property( 'message', - `getConfig()'s responseData.configFileContents must be a base64 string. ` + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); }); diff --git a/test/unit/project-management/ios-app.spec.ts b/test/unit/project-management/ios-app.spec.ts index ec300ea554..e0163d893f 100644 --- a/test/unit/project-management/ios-app.spec.ts +++ b/test/unit/project-management/ios-app.spec.ts @@ -21,11 +21,16 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { IosApp } from '../../../src/project-management/ios-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { IosAppMetadata, AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import IosAppMetadata = projectManagement.IosAppMetadata; +import AppPlatform = projectManagement.AppPlatform; const expect = chai.expect; @@ -108,7 +113,7 @@ describe('IosApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getResource') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return iosApp.getMetadata() .should.eventually.be.rejected @@ -183,7 +188,7 @@ describe('IosApp', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'getConfig') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return iosApp.getConfig() .should.eventually.be.rejected @@ -204,7 +209,7 @@ describe('IosApp', () => { .should.eventually.be.rejected .and.have.property( 'message', - `getConfig()'s responseData.configFileContents must be a base64 string. ` + 'getConfig()\'s responseData.configFileContents must be a base64 string. ' + `Response data: ${JSON.stringify(apiResponse, null, 2)}`); }); diff --git a/test/unit/project-management/project-management-api-request.spec.ts b/test/unit/project-management/project-management-api-request.spec.ts index f2978b4a34..f4088e0a1f 100644 --- a/test/unit/project-management/project-management-api-request.spec.ts +++ b/test/unit/project-management/project-management-api-request.spec.ts @@ -22,12 +22,17 @@ import * as _ from 'lodash'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { FirebaseApp } from '../../../src/firebase-app'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as mocks from '../../resources/mocks'; import * as utils from '../utils'; +import { getSdkVersion } from '../../../src/utils/index'; import { ShaCertificate } from '../../../src/project-management/android-app'; -import { AppPlatform } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AppPlatform = projectManagement.AppPlatform; chai.should(); chai.use(sinonChai); @@ -72,7 +77,7 @@ describe('ProjectManagementRequestHandler', () => { beforeEach(() => { mockApp = mocks.app(); expectedHeaders = { - 'X-Client-Version': 'Node/Admin/', + 'X-Client-Version': `Node/Admin/${getSdkVersion()}`, 'Authorization': 'Bearer ' + mockAccessToken, }; requestHandler = new ProjectManagementRequestHandler(mockApp); diff --git a/test/unit/project-management/project-management.spec.ts b/test/unit/project-management/project-management.spec.ts index bcd02f6d71..f7837389c7 100644 --- a/test/unit/project-management/project-management.spec.ts +++ b/test/unit/project-management/project-management.spec.ts @@ -22,11 +22,16 @@ import * as sinon from 'sinon'; import { FirebaseApp } from '../../../src/firebase-app'; import { AndroidApp } from '../../../src/project-management/android-app'; import { ProjectManagement } from '../../../src/project-management/project-management'; -import { ProjectManagementRequestHandler } from '../../../src/project-management/project-management-api-request'; +import { + ProjectManagementRequestHandler +} from '../../../src/project-management/project-management-api-request-internal'; import { FirebaseProjectManagementError } from '../../../src/utils/error'; import * as mocks from '../../resources/mocks'; -import { IosApp } from '../../../src/project-management/ios-app'; -import { AppPlatform, AppMetadata } from '../../../src/project-management/app-metadata'; +import { projectManagement } from '../../../src/project-management/index'; + +import AppMetadata = projectManagement.AppMetadata; +import AppPlatform = projectManagement.AppPlatform; +import IosApp = projectManagement.IosApp; const expect = chai.expect; @@ -126,7 +131,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listAndroidApps') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listAndroidApps() .should.eventually.be.rejected @@ -210,7 +215,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listIosApps') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listIosApps() .should.eventually.be.rejected @@ -307,7 +312,7 @@ describe('ProjectManagement', () => { it('should throw when initial API response is null', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'createAndroidApp') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.createAndroidApp(PACKAGE_NAME) .should.eventually.be.rejected @@ -355,7 +360,7 @@ describe('ProjectManagement', () => { it('should throw when initial API response is null', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'createIosApp') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.createIosApp(BUNDLE_ID) .should.eventually.be.rejected @@ -425,7 +430,7 @@ describe('ProjectManagement', () => { it('should throw with null API response', () => { const stub = sinon .stub(ProjectManagementRequestHandler.prototype, 'listAppMetadata') - .returns(Promise.resolve(null)); + .resolves(null as any); stubs.push(stub); return projectManagement.listAppMetadata() .should.eventually.be.rejected diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 9ff46c788b..c416715dcb 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -19,20 +19,22 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; +import { remoteConfig } from '../../../src/remote-config/index'; import { - RemoteConfigApiClient, - RemoteConfigTemplate, - TagColor, - ListVersionsResult, - Version, -} from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError } from '../../../src/remote-config/remote-config-utils'; + 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/firebase-app'; import { deepCopy } from '../../../src/utils/deep-copy'; +import { getSdkVersion } from '../../../src/utils/index'; + +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import Version = remoteConfig.Version; +import ListVersionsResult = remoteConfig.ListVersionsResult; const expect = chai.expect; @@ -47,13 +49,14 @@ describe('RemoteConfigApiClient', () => { }; const VALIDATION_ERROR_MESSAGES = [ - "[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value maps must be valid condition names.", - "[VERSION_MISMATCH]: Expected version 6, found 8 for project: 123456789012" + '[VALIDATION_ERROR]: [foo] are not valid condition names. All keys in all conditional value' + + ' maps must be valid condition names.', + '[VERSION_MISMATCH]: Expected version 6, found 8 for project: 123456789012' ]; const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, 'Accept-Encoding': 'gzip', }; @@ -120,7 +123,7 @@ describe('RemoteConfigApiClient', () => { { name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.PINK, + tagColor: 'PINK', }, ], parameters: { @@ -177,7 +180,7 @@ describe('RemoteConfigApiClient', () => { }); describe('getTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getTemplate() .should.eventually.be.rejectedWith(noProjectId); }); @@ -208,7 +211,7 @@ describe('RemoteConfigApiClient', () => { }); describe('getTemplateAtVersion', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getTemplateAtVersion(65) .should.eventually.be.rejectedWith(noProjectId); }); @@ -259,7 +262,7 @@ describe('RemoteConfigApiClient', () => { }); describe('validateTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.validateTemplate(REMOTE_CONFIG_TEMPLATE) .should.eventually.be.rejectedWith(noProjectId); }); @@ -338,19 +341,19 @@ describe('RemoteConfigApiClient', () => { error: { code: 400, message: message, - status: "FAILED_PRECONDITION" + 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.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); describe('publishTemplate', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.publishTemplate(REMOTE_CONFIG_TEMPLATE) .should.eventually.be.rejectedWith(noProjectId); }); @@ -433,19 +436,19 @@ describe('RemoteConfigApiClient', () => { error: { code: 400, message: message, - status: "FAILED_PRECONDITION" + 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.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); describe('rollback', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.rollback('60') .should.eventually.be.rejectedWith(noProjectId); }); @@ -500,7 +503,7 @@ describe('RemoteConfigApiClient', () => { }); describe('listVersions', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.listVersions() .should.eventually.be.rejectedWith(noProjectId); }); @@ -529,11 +532,19 @@ describe('RemoteConfigApiClient', () => { }); }); - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach( + ['', null, NaN, true, [], {}].forEach( + (invalidVersion) => { + it(`should throw if the endVersionNumber is: ${invalidVersion}`, () => { + expect(() => apiClient.listVersions({ endVersionNumber: invalidVersion } as any)) + .to.throw(/^endVersionNumber must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach( (invalidVersion) => { it(`should throw if the endVersionNumber is: ${invalidVersion}`, () => { expect(() => apiClient.listVersions({ endVersionNumber: invalidVersion } as any)) - .to.throw(/^endVersionNumber must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .to.throw(/^endVersionNumber must be an integer or a string in int64 format$/); }); }); @@ -655,10 +666,17 @@ describe('RemoteConfigApiClient', () => { }); function runTemplateVersionNumberTests(rcOperation: Function): void { - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach((invalidVersion) => { + ['', null, NaN, true, [], {}].forEach((invalidVersion) => { + it(`should reject if the versionNumber is: ${invalidVersion}`, () => { + expect(() => rcOperation(invalidVersion as any)) + .to.throw(/^versionNumber must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { expect(() => rcOperation(invalidVersion as any)) - .to.throw(/^versionNumber must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .to.throw(/^versionNumber must be an integer or a string in int64 format$/); }); }); } @@ -672,7 +690,7 @@ describe('RemoteConfigApiClient', () => { const expected = new FirebaseRemoteConfigError('invalid-argument', 'ETag header is not present in the server response.'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); } @@ -684,7 +702,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('not-found', 'Requested entity not found'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error when error code is not present', () => { @@ -694,7 +712,7 @@ describe('RemoteConfigApiClient', () => { stubs.push(stub); const expected = new FirebaseRemoteConfigError('unknown-error', 'Unknown server error: {}'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject with unknown-error for non-json response', () => { @@ -705,7 +723,7 @@ describe('RemoteConfigApiClient', () => { const expected = new FirebaseRemoteConfigError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should reject when rejected with a FirebaseAppError', () => { @@ -715,7 +733,7 @@ describe('RemoteConfigApiClient', () => { .rejects(expected); stubs.push(stub); return rcOperation() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); } diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index 0d05656507..6c946f41fa 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -22,16 +22,18 @@ import * as sinon from 'sinon'; import { RemoteConfig } from '../../../src/remote-config/remote-config'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; +import { remoteConfig } from '../../../src/remote-config/index'; import { - RemoteConfigApiClient, - RemoteConfigTemplate, - RemoteConfigCondition, - TagColor, - ListVersionsResult, -} from '../../../src/remote-config/remote-config-api-client'; -import { FirebaseRemoteConfigError } from '../../../src/remote-config/remote-config-utils'; + FirebaseRemoteConfigError, + RemoteConfigApiClient +} from '../../../src/remote-config/remote-config-api-client-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; +import RemoteConfigTemplate = remoteConfig.RemoteConfigTemplate; +import RemoteConfigCondition = remoteConfig.RemoteConfigCondition; +import TagColor = remoteConfig.TagColor; +import ListVersionsResult = remoteConfig.ListVersionsResult; + const expect = chai.expect; describe('RemoteConfig', () => { @@ -62,7 +64,7 @@ describe('RemoteConfig', () => { email: 'firebase-adminsdk@gserviceaccount.com' }, description: 'production version', - updateTime: '2020-06-15T16:45:03.000Z' + updateTime: '2020-06-15T16:45:03.541527Z' }; const REMOTE_CONFIG_RESPONSE: { @@ -80,7 +82,7 @@ describe('RemoteConfig', () => { { name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.BLUE, + tagColor: 'BLUE', }, ], parameters: { @@ -100,7 +102,7 @@ describe('RemoteConfig', () => { conditions: [{ name: 'ios', expression: 'device.os == \'ios\'', - tagColor: TagColor.PINK, + tagColor: 'PINK', }], parameters: { // eslint-disable-next-line @typescript-eslint/camelcase @@ -121,7 +123,7 @@ describe('RemoteConfig', () => { versions: [ { versionNumber: '78', - updateTime: '2020-05-07T18:46:09.495Z', + updateTime: '2020-05-07T18:46:09.495234Z', updateUser: { email: 'user@gmail.com', imageUrl: 'https://photo.jpg' @@ -255,7 +257,7 @@ describe('RemoteConfig', () => { .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); }); - ['', 'abc', 'a123b', 'a123', '123a', 1.2, '70.2', null, NaN, true, [], {}].forEach((invalidVersion) => { + ['', null, NaN, true, [], {}].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); response.versions[0].versionNumber = invalidVersion as any; @@ -264,7 +266,22 @@ describe('RemoteConfig', () => { .resolves(response); stubs.push(stub); return remoteConfig.listVersions() - .should.eventually.be.rejected.and.to.match(/^Error: Version number must be (a non-empty string in int64 format or a number|an integer or a string in int64 format)$/); + .should.eventually.be.rejected + .and.to.match(/^Error: Version number must be 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$/); }); }); @@ -369,7 +386,7 @@ describe('RemoteConfig', () => { it('should resolve with an empty versions list if the no results are availble for requested list options', () => { const stub = sinon .stub(RemoteConfigApiClient.prototype, 'listVersions') - .resolves({}); + .resolves({} as any); stubs.push(stub); return remoteConfig.listVersions({ pageSize: 2, @@ -416,7 +433,7 @@ describe('RemoteConfig', () => { INVALID_JSON_STRINGS.forEach((invalidJson) => { it(`should throw if the json string is ${JSON.stringify(invalidJson)}`, () => { expect(() => remoteConfig.createTemplateFromJSON(invalidJson)) - .to.throw(/^Failed to parse the JSON string: ([\D\w]*)\. SyntaxError: Unexpected token ([\D\w]*) in JSON at position ([0-9]*)$/); + .to.throw(/Failed to parse the JSON string: ([\D\w]*)\./); }); }); @@ -467,12 +484,12 @@ describe('RemoteConfig', () => { expect(newTemplate.conditions.length).to.equal(1); expect(newTemplate.conditions[0].name).to.equal('ios'); expect(newTemplate.conditions[0].expression).to.equal('device.os == \'ios\''); - expect(newTemplate.conditions[0].tagColor).to.equal(TagColor.BLUE); + expect(newTemplate.conditions[0].tagColor).to.equal('BLUE'); // verify that the etag is unchanged expect(newTemplate.etag).to.equal('etag-123456789012-5'); // verify that the etag is read-only expect(() => { - (newTemplate as any).etag = "new-etag"; + (newTemplate as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter'); @@ -489,7 +506,7 @@ describe('RemoteConfig', () => { const cond = c as RemoteConfigCondition; expect(cond.name).to.equal('ios'); expect(cond.expression).to.equal('device.os == \'ios\''); - expect(cond.tagColor).to.equal(TagColor.BLUE); + expect(cond.tagColor).to.equal('BLUE'); }); }); @@ -535,7 +552,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config parameters must be a non-null object`); + 'message', 'Remote Config parameters must be a non-null object'); }); it('should reject when API response does not contain valid parameter groups', () => { @@ -547,7 +564,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config parameter groups must be a non-null object`); + 'message', 'Remote Config parameter groups must be a non-null object'); }); it('should reject when API response does not contain valid conditions', () => { @@ -559,7 +576,7 @@ describe('RemoteConfig', () => { stubs.push(stub); return rcOperation() .should.eventually.be.rejected.and.have.property( - 'message', `Remote Config conditions must be an array`); + 'message', 'Remote Config conditions must be an array'); }); } @@ -622,11 +639,11 @@ describe('RemoteConfig', () => { expect(template.conditions.length).to.equal(1); expect(template.conditions[0].name).to.equal('ios'); expect(template.conditions[0].expression).to.equal('device.os == \'ios\''); - expect(template.conditions[0].tagColor).to.equal(TagColor.BLUE); + expect(template.conditions[0].tagColor).to.equal('BLUE'); expect(template.etag).to.equal('etag-123456789012-5'); // verify that etag is read-only expect(() => { - (template as any).etag = "new-etag"; + (template as any).etag = 'new-etag'; }).to.throw( 'Cannot set property etag of # which has only a getter'); @@ -653,7 +670,7 @@ describe('RemoteConfig', () => { const cond = c as RemoteConfigCondition; expect(cond.name).to.equal('ios'); expect(cond.expression).to.equal('device.os == \'ios\''); - expect(cond.tagColor).to.equal(TagColor.BLUE); + expect(cond.tagColor).to.equal('BLUE'); const parsed = JSON.parse(JSON.stringify(template)); const expectedTemplate = deepCopy(REMOTE_CONFIG_RESPONSE); @@ -663,5 +680,83 @@ describe('RemoteConfig', () => { 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/security-rules-api-client.spec.ts b/test/unit/security-rules/security-rules-api-client.spec.ts index ffa9230b7f..f0cd1c4185 100644 --- a/test/unit/security-rules/security-rules-api-client.spec.ts +++ b/test/unit/security-rules/security-rules-api-client.spec.ts @@ -19,13 +19,14 @@ import * as _ from 'lodash'; import * as chai from 'chai'; import * as sinon from 'sinon'; -import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-utils'; +import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; import { FirebaseAppError } from '../../../src/utils/error'; import { FirebaseApp } from '../../../src/firebase-app'; +import { getSdkVersion } from '../../../src/utils/index'; const expect = chai.expect; @@ -42,7 +43,7 @@ describe('SecurityRulesApiClient', () => { }; const EXPECTED_HEADERS = { 'Authorization': 'Bearer mock-token', - 'X-Firebase-Client': 'fire-admin-node/', + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, }; const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + 'account credentials, or set project ID as an app option. Alternatively, set the ' @@ -89,13 +90,13 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.getRuleset('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Ruleset name must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getRuleset(RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -123,7 +124,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -133,7 +134,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -144,7 +145,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -154,7 +155,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -179,7 +180,7 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.createRuleset({ source: { files: [RULES_FILE], @@ -237,7 +238,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when the rulesets limit reached', () => { @@ -254,7 +255,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('resource-exhausted', resourceExhaustedError.error.message); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -264,7 +265,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -275,7 +276,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -285,7 +286,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.createRuleset(RULES_CONTENT) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -327,7 +328,7 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.listRulesets() .should.eventually.be.rejectedWith(noProjectId); }); @@ -390,7 +391,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -400,7 +401,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -411,7 +412,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -421,12 +422,12 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.listRulesets() - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); describe('getRelease', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.getRelease(RELEASE_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -454,7 +455,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -464,7 +465,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -475,7 +476,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -485,12 +486,12 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.getRelease(RELEASE_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); describe('updateRelease', () => { - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.updateRelease(RELEASE_NAME, RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -524,7 +525,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -534,7 +535,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -545,7 +546,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -555,7 +556,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.updateRelease(RELEASE_NAME, RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); @@ -569,13 +570,13 @@ describe('SecurityRulesApiClient', () => { }); }); - it(`should reject when called with prefixed name`, () => { + it('should reject when called with prefixed name', () => { return apiClient.deleteRuleset('projects/foo/rulesets/bar') .should.eventually.be.rejected.and.have.property( 'message', 'Ruleset name must not contain any "/" characters.'); }); - it(`should reject when project id is not available`, () => { + it('should reject when project id is not available', () => { return clientWithoutProjectId.deleteRuleset(RULESET_NAME) .should.eventually.be.rejectedWith(noProjectId); }); @@ -602,7 +603,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error when error code is not present', () => { @@ -612,7 +613,7 @@ describe('SecurityRulesApiClient', () => { stubs.push(stub); const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw unknown-error for non-json response', () => { @@ -623,7 +624,7 @@ describe('SecurityRulesApiClient', () => { const expected = new FirebaseSecurityRulesError( 'unknown-error', 'Unexpected response with status: 404 and body: not json'); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); it('should throw when rejected with a FirebaseAppError', () => { @@ -633,7 +634,7 @@ describe('SecurityRulesApiClient', () => { .rejects(expected); stubs.push(stub); return apiClient.deleteRuleset(RULESET_NAME) - .should.eventually.be.rejected.and.deep.equal(expected); + .should.eventually.be.rejected.and.deep.include(expected); }); }); }); diff --git a/test/unit/security-rules/security-rules.spec.ts b/test/unit/security-rules/security-rules.spec.ts index 45e1f98a87..fd81fa7fd2 100644 --- a/test/unit/security-rules/security-rules.spec.ts +++ b/test/unit/security-rules/security-rules.spec.ts @@ -22,8 +22,8 @@ import * as sinon from 'sinon'; import { SecurityRules } from '../../../src/security-rules/security-rules'; import { FirebaseApp } from '../../../src/firebase-app'; import * as mocks from '../../resources/mocks'; -import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client'; -import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-utils'; +import { SecurityRulesApiClient, RulesetContent } from '../../../src/security-rules/security-rules-api-client-internal'; +import { FirebaseSecurityRulesError } from '../../../src/security-rules/security-rules-internal'; import { deepCopy } from '../../../src/utils/deep-copy'; const expect = chai.expect; @@ -31,15 +31,7 @@ const expect = chai.expect; describe('SecurityRules', () => { const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message'); - const FIRESTORE_RULESET_RESPONSE: { - // This type is effectively a RulesetResponse, but with non-readonly fields - // to allow easier use from within the tests. An improvement would be to - // alter this into a helper that creates customized RulesetResponses based - // on the needs of the test, as that would ensure type-safety. - name: string; - createTime: string; - source: object | null; - } = { + const FIRESTORE_RULESET_RESPONSE = { name: 'projects/test-project/rulesets/foo', createTime: '2019-03-08T23:45:23.288047Z', source: { @@ -51,13 +43,17 @@ describe('SecurityRules', () => { ], }, }; + const FIRESTORE_RULESET_RELEASE = { + name: 'projects/test-project/releases/firestore.release', + rulesetName: 'projects/test-project/rulesets/foo', + }; const CREATE_TIME_UTC = 'Fri, 08 Mar 2019 23:45:23 GMT'; const INVALID_RULESET_ERROR = new FirebaseSecurityRulesError( 'invalid-argument', 'ruleset must be a non-empty name or a RulesetMetadata object.', ); - const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], {name: ''}]; + const INVALID_RULESETS: any[] = [null, undefined, '', 1, true, {}, [], { name: '' }]; const INVALID_BUCKET_ERROR = new FirebaseSecurityRulesError( 'invalid-argument', @@ -99,9 +95,7 @@ describe('SecurityRules', () => { .resolves(FIRESTORE_RULESET_RESPONSE); const updateRelease = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(createRuleset, updateRelease); return [createRuleset, updateRelease]; } @@ -161,13 +155,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.getRuleset('foo') .should.eventually.be.rejected.and.have.property( @@ -200,7 +194,7 @@ describe('SecurityRules', () => { it('should reject when API response does not contain a source', () => { const response = deepCopy(FIRESTORE_RULESET_RESPONSE); - response.source = null; + response.source = null as any; const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(response); @@ -236,13 +230,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getFirestoreRuleset() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when getRelease response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({}); + .resolves({} as any); stubs.push(stub); return securityRules.getFirestoreRuleset() @@ -253,9 +247,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -281,7 +273,7 @@ describe('SecurityRules', () => { INVALID_BUCKET_NAMES.forEach((bucketName) => { it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { return securityRules.getStorageRuleset(bucketName) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -291,13 +283,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.getStorageRuleset() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when getRelease response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({}); + .resolves({} as any); stubs.push(stub); return securityRules.getStorageRuleset() @@ -308,9 +300,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset for the default bucket on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -334,9 +324,7 @@ describe('SecurityRules', () => { it('should resolve with Ruleset for the specified bucket on success', () => { const getRelease = sinon .stub(SecurityRulesApiClient.prototype, 'getRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); const getRuleset = sinon .stub(SecurityRulesApiClient.prototype, 'getRuleset') .resolves(FIRESTORE_RULESET_RESPONSE); @@ -362,7 +350,7 @@ describe('SecurityRules', () => { INVALID_RULESETS.forEach((invalidRuleset) => { it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { return securityRules.releaseFirestoreRuleset(invalidRuleset) - .should.eventually.be.rejected.and.deep.equal(INVALID_RULESET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_RULESET_ERROR); }); }); @@ -372,15 +360,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseFirestoreRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success when the ruleset specified by name', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseFirestoreRuleset('foo') @@ -392,12 +378,10 @@ describe('SecurityRules', () => { it('should resolve on success when the ruleset specified as an object', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); - return securityRules.releaseFirestoreRuleset({name: 'foo', createTime: 'time'}) + return securityRules.releaseFirestoreRuleset({ name: 'foo', createTime: 'time' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith('cloud.firestore', 'foo'); }); @@ -413,7 +397,7 @@ describe('SecurityRules', () => { INVALID_SOURCES.forEach((invalidSource) => { it(`should reject when called with: ${JSON.stringify(invalidSource)}`, () => { return securityRules.releaseFirestoreRulesetFromSource(invalidSource) - .should.eventually.be.rejected.and.deep.equal(INVALID_SOURCE_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_SOURCE_ERROR); }); }); @@ -423,7 +407,7 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseFirestoreRulesetFromSource('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); const sources: {[key: string]: string | Buffer} = { @@ -462,14 +446,14 @@ describe('SecurityRules', () => { INVALID_RULESETS.forEach((invalidRuleset) => { it(`should reject when called with: ${JSON.stringify(invalidRuleset)}`, () => { return securityRules.releaseStorageRuleset(invalidRuleset) - .should.eventually.be.rejected.and.deep.equal(INVALID_RULESET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_RULESET_ERROR); }); }); INVALID_BUCKET_NAMES.forEach((bucketName) => { it(`should reject when called with: ${JSON.stringify(bucketName)}`, () => { return securityRules.releaseStorageRuleset('foo', bucketName) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -479,15 +463,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseStorageRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success when the ruleset specified by name', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseStorageRuleset('foo') @@ -500,9 +482,7 @@ describe('SecurityRules', () => { it('should resolve on success when a custom bucket name is specified', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); return securityRules.releaseStorageRuleset('foo', 'other.appspot.com') @@ -515,12 +495,10 @@ describe('SecurityRules', () => { it('should resolve on success when the ruleset specified as an object', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'updateRelease') - .resolves({ - rulesetName: 'projects/test-project/rulesets/foo', - }); + .resolves(FIRESTORE_RULESET_RELEASE); stubs.push(stub); - return securityRules.releaseStorageRuleset({name: 'foo', createTime: 'time'}) + return securityRules.releaseStorageRuleset({ name: 'foo', createTime: 'time' }) .then(() => { expect(stub).to.have.been.calledOnce.and.calledWith( 'firebase.storage/bucketName.appspot.com', 'foo'); @@ -544,14 +522,14 @@ describe('SecurityRules', () => { INVALID_SOURCES.forEach((invalidSource) => { it(`should reject when called with source: ${JSON.stringify(invalidSource)}`, () => { return securityRules.releaseStorageRulesetFromSource(invalidSource) - .should.eventually.be.rejected.and.deep.equal(INVALID_SOURCE_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_SOURCE_ERROR); }); }); INVALID_BUCKET_NAMES.forEach((invalidBucket) => { it(`should reject when called with bucket: ${JSON.stringify(invalidBucket)}`, () => { return securityRules.releaseStorageRulesetFromSource(RULES_FILE.content, invalidBucket) - .should.eventually.be.rejected.and.deep.equal(INVALID_BUCKET_ERROR); + .should.eventually.be.rejected.and.deep.include(INVALID_BUCKET_ERROR); }); }); @@ -561,7 +539,7 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.releaseStorageRulesetFromSource('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); const sources: {[key: string]: string | Buffer} = { @@ -654,13 +632,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.createRuleset(RULES_FILE) - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'createRuleset') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.createRuleset(RULES_FILE) .should.eventually.be.rejected.and.have.property( @@ -726,13 +704,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.deleteRuleset('foo') - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should resolve on success', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'deleteRuleset') - .resolves({}); + .resolves(); stubs.push(stub); return securityRules.deleteRuleset('foo'); @@ -760,13 +738,13 @@ describe('SecurityRules', () => { .rejects(EXPECTED_ERROR); stubs.push(stub); return securityRules.listRulesetMetadata() - .should.eventually.be.rejected.and.deep.equal(EXPECTED_ERROR); + .should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR); }); it('should reject when API response is invalid', () => { const stub = sinon .stub(SecurityRulesApiClient.prototype, 'listRulesets') - .resolves(null); + .resolves(null as any); stubs.push(stub); return securityRules.listRulesetMetadata() .should.eventually.be.rejected.and.have.property( diff --git a/test/unit/storage/storage.spec.ts b/test/unit/storage/storage.spec.ts index 55fc8ff6df..e37bffd0e4 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/firebase-app'; +import { Storage } from '../../../src/storage/storage'; describe('Storage', () => { let mockApp: FirebaseApp; @@ -90,7 +91,7 @@ describe('Storage', () => { const expectedError = 'Bucket name not specified or invalid. Specify a valid bucket name via ' + 'the storageBucket option when initializing the app, or specify the bucket name ' + 'explicitly when calling the getBucket() method.'; - const invalidNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1}, _.noop]; + const invalidNames = [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop]; invalidNames.forEach((invalidName) => { it(`should throw given invalid bucket name: ${ JSON.stringify(invalidName) }`, () => { expect(() => { @@ -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 12bc260b65..6eb845831a 100644 --- a/test/unit/utils.ts +++ b/test/unit/utils.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,19 +20,20 @@ import * as sinon from 'sinon'; import * as mocks from '../resources/mocks'; -import {FirebaseNamespace} from '../../src/firebase-namespace'; -import {FirebaseApp, FirebaseAppOptions, FirebaseAppInternals, FirebaseAccessToken} from '../../src/firebase-app'; +import { FirebaseNamespace } from '../../src/firebase-namespace'; +import { AppOptions } from '../../src/firebase-namespace-api'; +import { FirebaseApp, FirebaseAppInternals, FirebaseAccessToken } from '../../src/firebase-app'; import { HttpError, HttpResponse } from '../../src/utils/api-request'; /** * Returns a new FirebaseApp instance with the provided options. * - * @param {object} options The options for the FirebaseApp instance to create. - * @return {FirebaseApp} A new FirebaseApp instance with the provided options. + * @param options The options for the FirebaseApp instance to create. + * @return A new FirebaseApp instance with the provided options. */ export function createAppWithOptions(options: object): FirebaseApp { const mockFirebaseNamespaceInternals = new FirebaseNamespace().INTERNAL; - return new FirebaseApp(options as FirebaseAppOptions, mocks.appName, mockFirebaseNamespaceInternals); + return new FirebaseApp(options as AppOptions, mocks.appName, mockFirebaseNamespaceInternals); } diff --git a/test/unit/utils/api-request.spec.ts b/test/unit/utils/api-request.spec.ts index c2798de455..f997cfc9dd 100644 --- a/test/unit/utils/api-request.spec.ts +++ b/test/unit/utils/api-request.spec.ts @@ -1,4 +1,5 @@ /*! + * @license * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,13 +26,13 @@ import * as chaiAsPromised from 'chai-as-promised'; import * as utils from '../utils'; import * as mocks from '../../resources/mocks'; -import {FirebaseApp} from '../../../src/firebase-app'; +import { FirebaseApp } from '../../../src/firebase-app'; import { ApiSettings, HttpClient, HttpError, AuthorizedHttpClient, ApiCallbackFunction, HttpRequestConfig, HttpResponse, parseHttpResponse, RetryConfig, defaultRetryConfig, } from '../../../src/utils/api-request'; import { deepCopy } from '../../../src/utils/deep-copy'; -import {Agent} from 'http'; +import { Agent } from 'http'; import * as zlib from 'zlib'; chai.should(); @@ -141,7 +142,7 @@ describe('HttpClient', () => { invalidNumbers.forEach((maxRetries: any) => { it(`should throw when maxRetries is: ${maxRetries}`, () => { expect(() => { - new HttpClient({maxRetries} as any); + new HttpClient({ maxRetries } as any); }).to.throw('maxRetries must be a non-negative integer'); }); }); @@ -150,7 +151,7 @@ describe('HttpClient', () => { if (typeof backOffFactor !== 'undefined') { it(`should throw when backOffFactor is: ${backOffFactor}`, () => { expect(() => { - new HttpClient({maxRetries: 1, backOffFactor} as any); + new HttpClient({ maxRetries: 1, backOffFactor } as any); }).to.throw('backOffFactor must be a non-negative number'); }); } @@ -159,7 +160,7 @@ describe('HttpClient', () => { invalidNumbers.forEach((maxDelayInMillis: any) => { it(`should throw when maxDelayInMillis is: ${maxDelayInMillis}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis } as any); }).to.throw('maxDelayInMillis must be a non-negative integer'); }); }); @@ -167,7 +168,7 @@ describe('HttpClient', () => { invalidArrays.forEach((ioErrorCodes: any) => { it(`should throw when ioErrorCodes is: ${ioErrorCodes}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis: 10000, ioErrorCodes} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, ioErrorCodes } as any); }).to.throw('ioErrorCodes must be an array'); }); }); @@ -175,13 +176,13 @@ describe('HttpClient', () => { invalidArrays.forEach((statusCodes: any) => { it(`should throw when statusCodes is: ${statusCodes}`, () => { expect(() => { - new HttpClient({maxRetries: 1, maxDelayInMillis: 10000, statusCodes} as any); + new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, statusCodes } as any); }).to.throw('statusCodes must be an array'); }); }); it('should be fulfilled for a 2xx response with a json payload', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -337,7 +338,7 @@ describe('HttpClient', () => { }); it('should use the specified HTTP agent', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -369,8 +370,8 @@ describe('HttpClient', () => { }); it('should make a POST request with the provided headers and data', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -402,8 +403,8 @@ describe('HttpClient', () => { }); it('should use the specified content-type header for the body', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Content-Type': (header) => { @@ -432,7 +433,7 @@ describe('HttpClient', () => { }); it('should not mutate the arguments', () => { - const reqData = {request: 'data'}; + const reqData = { request: 'data' }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -442,7 +443,7 @@ describe('HttpClient', () => { 'My-Custom-Header': 'CustomValue', }, }).post(mockPath, reqData) - .reply(200, {success: true}, { + .reply(200, { success: true }, { 'content-type': 'application/json', }); mockedRequests.push(scope); @@ -464,8 +465,8 @@ describe('HttpClient', () => { }); it('should make a GET request with the provided headers and data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -495,9 +496,9 @@ describe('HttpClient', () => { }); it('should merge query parameters in URL with data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const mergedData = {...reqData, key3: 'value3'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const mergedData = { ...reqData, key3: 'value3' }; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .query(mergedData) @@ -519,9 +520,9 @@ describe('HttpClient', () => { }); it('should urlEncode query parameters in URL', () => { - const reqData = {key1: 'value 1!', key2: 'value 2!'}; - const mergedData = {...reqData, key3: 'value 3!'}; - const respData = {success: true}; + const reqData = { key1: 'value 1!', key2: 'value 2!' }; + const mergedData = { ...reqData, key3: 'value 3!' }; + const respData = { success: true }; const scope = nock('https://' + mockHost) .get(mockPath) .query(mergedData) @@ -543,7 +544,7 @@ describe('HttpClient', () => { }); it('should default to https when protocol not specified', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -576,8 +577,8 @@ describe('HttpClient', () => { }); it('should make a HEAD request with the provided headers and data', () => { - const reqData = {key1: 'value1', key2: 'value2'}; - const respData = {success: true}; + const reqData = { key1: 'value1', key2: 'value2' }; + const respData = { success: true }; const scope = nock('https://' + mockHost, { reqheaders: { 'Authorization': 'Bearer token', @@ -618,7 +619,7 @@ describe('HttpClient', () => { }); it('should fail with an HttpError for a 4xx response', () => { - const data = {error: 'data'}; + const data = { error: 'data' }; mockedRequests.push(mockRequestWithHttpError(400, 'application/json', data)); const client = new HttpClient(); return client.send({ @@ -635,7 +636,7 @@ describe('HttpClient', () => { }); it('should fail with an HttpError for a 5xx response', () => { - const data = {error: 'data'}; + const data = { error: 'data' }; mockedRequests.push(mockRequestWithHttpError(500, 'application/json', data)); const client = new HttpClient(); return client.send({ @@ -676,7 +677,7 @@ describe('HttpClient', () => { }); it('should fail with a FirebaseAppError for a network error', () => { - mockedRequests.push(mockRequestWithError({message: 'test error', code: 'AWFUL_ERROR'})); + mockedRequests.push(mockRequestWithError({ message: 'test error', code: 'AWFUL_ERROR' })); const client = new HttpClient(); const err = 'Error while making request: test error. Error code: AWFUL_ERROR'; return client.send({ @@ -686,7 +687,7 @@ describe('HttpClient', () => { }); it('should timeout when the response is repeatedly delayed', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .times(5) @@ -707,11 +708,11 @@ describe('HttpClient', () => { }); it('should timeout when multiple socket timeouts encountered', () => { - const respData = {foo: 'bar timeout'}; + const respData = { foo: 'bar timeout' }; const scope = nock('https://' + mockHost) .get(mockPath) .times(5) - .socketDelay(2000) + .delayConnection(2000) .reply(200, respData, { 'content-type': 'application/json', }); @@ -729,7 +730,7 @@ describe('HttpClient', () => { it('should be rejected, after 4 retries, on multiple network errors', () => { for (let i = 0; i < 5; i++) { - mockedRequests.push(mockRequestWithError({message: `connection reset ${i + 1}`, code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: `connection reset ${i + 1}`, code: 'ECONNRESET' })); } const client = new HttpClient(testRetryConfig()); @@ -767,8 +768,8 @@ describe('HttpClient', () => { }); it('should succeed, after 1 retry, on a single network error', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); - const respData = {foo: 'bar'}; + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -786,7 +787,7 @@ describe('HttpClient', () => { }); it('should not retry when RetryConfig is explicitly null', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient(null); const err = 'Error while making request: connection reset 1'; return client.send({ @@ -796,7 +797,7 @@ describe('HttpClient', () => { }); it('should not retry when maxRetries is set to 0', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient({ maxRetries: 0, ioErrorCodes: ['ECONNRESET'], @@ -810,7 +811,7 @@ describe('HttpClient', () => { }); it('should not retry when error codes are not configured', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); const client = new HttpClient({ maxRetries: 1, maxDelayInMillis: 10000, @@ -823,8 +824,8 @@ describe('HttpClient', () => { }); it('should succeed after a retry on a configured I/O error', () => { - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ETESTCODE'})); - const respData = {foo: 'bar'}; + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ETESTCODE' })); + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -852,7 +853,7 @@ describe('HttpClient', () => { 'content-type': 'application/json', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -871,8 +872,8 @@ describe('HttpClient', () => { it('should not retry more than maxRetries', () => { // simulate 2 low-level errors - mockedRequests.push(mockRequestWithError({message: 'connection reset 1', code: 'ECONNRESET'})); - mockedRequests.push(mockRequestWithError({message: 'connection reset 2', code: 'ECONNRESET'})); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 1', code: 'ECONNRESET' })); + mockedRequests.push(mockRequestWithError({ message: 'connection reset 2', code: 'ECONNRESET' })); // followed by 3 HTTP errors const scope = nock('https://' + mockHost) @@ -1020,7 +1021,7 @@ describe('HttpClient', () => { 'retry-after': '30', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1056,7 +1057,7 @@ describe('HttpClient', () => { 'retry-after': timestamp.toUTCString(), }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1090,7 +1091,7 @@ describe('HttpClient', () => { 'retry-after': timestamp.toUTCString(), }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1122,7 +1123,7 @@ describe('HttpClient', () => { 'retry-after': 'invalid', }); mockedRequests.push(scope1); - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope2 = nock('https://' + mockHost) .get(mockPath) .reply(200, respData, { @@ -1158,7 +1159,7 @@ describe('HttpClient', () => { }); it('should use the port 80 for http URLs', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('http://' + mockHost + ':80') .get('/') .reply(200, respData, { @@ -1175,7 +1176,7 @@ describe('HttpClient', () => { }); it('should use the port specified in the URL', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost + ':8080') .get('/') .reply(200, respData, { @@ -1223,7 +1224,7 @@ describe('AuthorizedHttpClient', () => { }); it('should be fulfilled for a 2xx response with a json payload', () => { - const respData = {foo: 'bar'}; + const respData = { foo: 'bar' }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1265,7 +1266,7 @@ describe('AuthorizedHttpClient', () => { }); it('should use the HTTP agent set in request', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1287,7 +1288,7 @@ describe('AuthorizedHttpClient', () => { }); it('should use the HTTP agent set in AppOptions', () => { - const respData = {success: true}; + const respData = { success: true }; const scope = nock('https://' + mockHost, requestHeaders) .get(mockPath) .reply(200, respData, { @@ -1308,8 +1309,8 @@ describe('AuthorizedHttpClient', () => { }); it('should make a POST request with the provided headers and data', () => { - const reqData = {request: 'data'}; - const respData = {success: true}; + const reqData = { request: 'data' }; + const respData = { success: true }; const options = { reqheaders: { 'Content-Type': (header: string) => { @@ -1341,7 +1342,7 @@ describe('AuthorizedHttpClient', () => { }); it('should not mutate the arguments', () => { - const reqData = {request: 'data'}; + const reqData = { request: 'data' }; const options = { reqheaders: { 'Content-Type': (header: string) => { @@ -1353,7 +1354,7 @@ describe('AuthorizedHttpClient', () => { Object.assign(options.reqheaders, requestHeaders.reqheaders); const scope = nock('https://' + mockHost, options) .post(mockPath, reqData) - .reply(200, {success: true}, { + .reply(200, { success: true }, { 'content-type': 'application/json', }); mockedRequests.push(scope); @@ -1464,7 +1465,7 @@ describe('parseHttpResponse()', () => { expect(response.headers).to.have.property('content-type', 'application/json'); expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); expect(response.isJson()).to.be.true; - expect(response.data).to.deep.equal({foo: 1}); + expect(response.data).to.deep.equal({ foo: 1 }); expect(response.text).to.equal('{"foo": 1}'); }); @@ -1482,7 +1483,7 @@ describe('parseHttpResponse()', () => { expect(response.headers).to.have.property('content-type', 'application/json'); expect(response.headers).to.have.property('date', 'Thu, 07 Feb 2019 19:20:34 GMT'); expect(response.isJson()).to.be.true; - expect(response.data).to.deep.equal({foo: 1}); + expect(response.data).to.deep.equal({ foo: 1 }); expect(response.text).to.equal('{"foo": 1}'); }); diff --git a/test/unit/utils/crypto-signer.spec.ts b/test/unit/utils/crypto-signer.spec.ts new file mode 100644 index 0000000000..9a59fd10eb --- /dev/null +++ b/test/unit/utils/crypto-signer.spec.ts @@ -0,0 +1,224 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { ServiceAccountSigner, IAMSigner, CryptoSignerError } from '../../../src/utils/crypto-signer'; + +import { ServiceAccountCredential } from '../../../src/credential/credential-internal'; +import { AuthorizedHttpClient, HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/firebase-app'; +import * as utils from '../utils'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('CryptoSigner', () => { + describe('ServiceAccountSigner', () => { + it('should throw given no arguments', () => { + expect(() => { + const anyServiceAccountSigner: any = ServiceAccountSigner; + return new anyServiceAccountSigner(); + }).to.throw('Must provide a service account credential to initialize ServiceAccountSigner'); + }); + + it('should not throw given a valid certificate', () => { + expect(() => { + return new ServiceAccountSigner(new ServiceAccountCredential(mocks.certificateObject)); + }).not.to.throw(); + }); + + it('should sign using the private_key in the certificate', () => { + const payload = Buffer.from('test'); + const cert = new ServiceAccountCredential(mocks.certificateObject); + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const crypto = require('crypto'); + const rsa = crypto.createSign('RSA-SHA256'); + rsa.update(payload); + const result = rsa.sign(cert.privateKey, 'base64'); + + const signer = new ServiceAccountSigner(cert); + return signer.sign(payload).then((signature) => { + expect(signature.toString('base64')).to.equal(result); + }); + }); + + it('should return the client_email from the certificate', () => { + const cert = new ServiceAccountCredential(mocks.certificateObject); + const signer = new ServiceAccountSigner(cert); + return signer.getAccountId().should.eventually.equal(cert.clientEmail); + }); + }); + + describe('IAMSigner', () => { + let mockApp: FirebaseApp; + let getTokenStub: sinon.SinonStub; + const mockAccessToken: string = utils.generateRandomAccessToken(); + + beforeEach(() => { + mockApp = mocks.app(); + getTokenStub = utils.stubGetAccessToken(mockAccessToken, mockApp); + return mockApp.INTERNAL.getToken(); + }); + + afterEach(() => { + getTokenStub.restore(); + return mockApp.delete(); + }); + + it('should throw given no arguments', () => { + expect(() => { + const anyIAMSigner: any = IAMSigner; + return new anyIAMSigner(); + }).to.throw('Must provide a HTTP client to initialize IAMSigner'); + }); + + describe('explicit service account ID', () => { + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; + const input = Buffer.from('input'); + const signRequest = { + method: 'POST', + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-service-account:signBlob', + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, + }; + let stub: sinon.SinonStub; + + afterEach(() => { + stub.restore(); + }); + + it('should sign using the IAM service', () => { + const expectedResult = utils.responseFrom(response); + stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler, 'test-service-account'); + return signer.sign(input).then((signature) => { + expect(signature.toString('base64')).to.equal(response.signedBlob); + expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); + }); + }); + + it('should fail if the IAM service responds with an error', () => { + const expectedResult = utils.errorFrom({ + error: { + status: 'PROJECT_NOT_FOUND', + message: 'test reason', + }, + }); + stub = sinon.stub(HttpClient.prototype, 'send').rejects(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler, 'test-service-account'); + return signer.sign(input).catch((err) => { + expect(err).to.be.instanceOf(CryptoSignerError); + expect(err.message).to.equal('Server responded with status 500.'); + expect(err.cause).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledOnce.and.calledWith(signRequest); + }); + }); + + it('should return the explicitly specified service account', () => { + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp), 'test-service-account'); + return signer.getAccountId().should.eventually.equal('test-service-account'); + }); + }); + + describe('auto discovered service account', () => { + const input = Buffer.from('input'); + const response = { signedBlob: Buffer.from('testsignature').toString('base64') }; + const metadataRequest = { + method: 'GET', + url: 'http://metadata/computeMetadata/v1/instance/service-accounts/default/email', + headers: { 'Metadata-Flavor': 'Google' }, + }; + const signRequest = { + method: 'POST', + url: 'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/discovered-service-account:signBlob', + headers: { Authorization: `Bearer ${mockAccessToken}` }, + data: { payload: input.toString('base64') }, + }; + let stub: sinon.SinonStub; + + afterEach(() => { + stub.restore(); + }); + + it('should sign using the IAM service', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + stub.onCall(1).resolves(utils.responseFrom(response)); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler); + return signer.sign(input).then((signature) => { + expect(signature.toString('base64')).to.equal(response.signedBlob); + expect(stub).to.have.been.calledTwice; + expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); + expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); + }); + }); + + it('should fail if the IAM service responds with an error', () => { + const expectedResult = utils.errorFrom({ + error: { + status: 'PROJECT_NOT_FOUND', + message: 'test reason', + }, + }); + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + stub.onCall(1).rejects(expectedResult); + const requestHandler = new AuthorizedHttpClient(mockApp); + const signer = new IAMSigner(requestHandler); + return signer.sign(input).catch((err) => { + expect(err).to.be.instanceOf(CryptoSignerError); + expect(err.message).to.equal('Server responded with status 500.'); + expect(err.cause).to.deep.equal(expectedResult); + expect(stub).to.have.been.calledTwice; + expect(stub.getCall(0).args[0]).to.deep.equal(metadataRequest); + expect(stub.getCall(1).args[0]).to.deep.equal(signRequest); + }); + }); + + it('should return the discovered service account', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).resolves(utils.responseFrom('discovered-service-account')); + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); + return signer.getAccountId().should.eventually.equal('discovered-service-account'); + }); + + it('should return the expected error when failed to contact the Metadata server', () => { + stub = sinon.stub(HttpClient.prototype, 'send'); + stub.onCall(0).rejects(utils.errorFrom('test error')); + const signer = new IAMSigner(new AuthorizedHttpClient(mockApp)); + const expected = 'Failed to determine service account. Make sure to initialize the SDK with ' + + 'a service account credential. Alternatively specify a service account with ' + + 'iam.serviceAccounts.signBlob permission.'; + return signer.getAccountId().should.eventually.be.rejectedWith(expected); + }); + }); + }); +}); diff --git a/test/unit/utils/error.spec.ts b/test/unit/utils/error.spec.ts old mode 100755 new mode 100644 index 41e977f725..c2321005ee --- 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 }); }); }); diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts old mode 100755 new mode 100644 index 2065e2fb49..9729dc7817 --- 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"); @@ -15,25 +16,33 @@ */ import * as _ from 'lodash'; -import {expect} from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { addReadonlyGetter, getExplicitProjectId, findProjectId, - toWebSafeBase64, formatString, generateUpdateMask, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, } from '../../../src/utils/index'; -import {isNonEmptyString} from '../../../src/utils/validator'; -import {FirebaseApp, FirebaseAppOptions} from '../../../src/firebase-app'; -import { ComputeEngineCredential } from '../../../src/auth/credential'; +import { isNonEmptyString } from '../../../src/utils/validator'; +import { FirebaseApp } from '../../../src/firebase-app'; +import { ComputeEngineCredential } from '../../../src/credential/credential-internal'; import { HttpClient } from '../../../src/utils/api-request'; import * as utils from '../utils'; 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 = {}; @@ -96,7 +105,7 @@ describe('getExplicitProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; @@ -164,7 +173,7 @@ describe('findProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; @@ -238,7 +247,7 @@ describe('findProjectId()', () => { }); it('should return the explicitly specified project ID from app options', () => { - const options: FirebaseAppOptions = { + const options = { credential: new mocks.MockCredential(), projectId: 'explicit-project-id', }; @@ -327,10 +336,32 @@ describe('formatString()', () => { }); describe('generateUpdateMask()', () => { + const obj: any = { + a: undefined, + b: 'something', + c: ['stuff'], + d: false, + e: {}, + f: { + g: 1, + h: 0, + i: { + j: 2, + }, + }, + k: { + i: null, + j: undefined, + }, + l: { + m: undefined, + }, + n: [], + }; const nonObjects = [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], _.noop]; nonObjects.forEach((nonObject) => { it(`should return empty array for non object ${JSON.stringify(nonObject)}`, () => { - expect(generateUpdateMask(nonObject as any)).to.deep.equal([]); + expect(generateUpdateMask(nonObject)).to.deep.equal([]); }); }); @@ -339,30 +370,29 @@ describe('generateUpdateMask()', () => { }); it('should return expected update mask array for nested object', () => { - const obj: any = { - a: undefined, - b: 'something', - c: ['stuff'], - d: false, - e: {}, - f: { - g: 1, - h: 0, - i: { - j: 2, - }, - }, - k: { - i: null, - j: undefined, - }, - l: { - m: undefined, - }, - }; const expectedMaskArray = [ - 'b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i.j', 'k.i', 'l', + 'b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i.j', 'k.i', 'l', 'n', ]; expect(generateUpdateMask(obj)).to.deep.equal(expectedMaskArray); }); + + it('should return expected update mask array with max paths for nested object', () => { + expect(generateUpdateMask(obj, ['f.i', 'k'])) + .to.deep.equal(['b', 'c', 'd', 'e', 'f.g', 'f.h', 'f.i', 'k', 'l', 'n']); + expect(generateUpdateMask(obj, ['notfound', 'b', 'f', 'k', 'l'])) + .to.deep.equal(['b', 'c', 'd', 'e', 'f', 'k', 'l', 'n']); + }); +}); + + +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]); + }); + }); }); diff --git a/test/unit/utils/jwt.spec.ts b/test/unit/utils/jwt.spec.ts new file mode 100644 index 0000000000..775bdd63b9 --- /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 ONE_DAY_IN_SECONDS = 86400; +const publicCertPath = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'; +const jwksPath = '/v1alpha/jwks'; + +/** + * Returns a mocked out success response from the URL containing the public keys for the Google certs. + * + * @param {string=} path URL path to which the mock request should be made. If not specified, defaults + * to the URL path of ID token public key certificates. + * @return {Object} A nock response object. + */ +function mockFetchPublicKeys(path: string = publicCertPath): nock.Scope { + const mockedResponse: { [key: string]: string } = {}; + mockedResponse[mocks.certificateObject.private_key_id] = mocks.keyPairs[0].public; + return nock('https://www.googleapis.com') + .get(path) + .reply(200, mockedResponse, { + 'cache-control': 'public, max-age=1, must-revalidate, no-transform', + }); +} + +/** + * Returns a mocked out error response from the URL containing the public keys for the Google certs. + * The status code is 200 but the response itself will contain an 'error' key. + * + * @return {Object} A nock response object. + */ + +function mockFetchPublicKeysWithErrorResponse(): nock.Scope { + return nock('https://www.googleapis.com') + .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') + .reply(200, { + error: 'message', + error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase + }); +} + +/** + * Returns a mocked out failed response from the URL containing the public keys for the Google certs. + * The status code is non-200 and the response itself will fail. + * + * @return {Object} A nock response object. + */ + +function mockFailedFetchPublicKeys(): nock.Scope { + return nock('https://www.googleapis.com') + .get('/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com') + .replyWithError('message'); +} + +/** + * Returns a mocked out success JWKS response. + * + * @return {Object} A nock response object. + */ +function mockFetchJsonWebKeys(path: string = jwksPath): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(path) + .reply(200, mocks.jwksResponse); +} + +/** + * Returns a mocked out error response for JWKS. + * The status code is 200 but the response itself will contain an 'error' key. + * + * @return {Object} A nock response object. + */ +function mockFetchJsonWebKeysWithErrorResponse(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .reply(200, { + error: 'message', + error_description: 'description', // eslint-disable-line @typescript-eslint/camelcase + }); +} + +/** + * Returns a mocked out failed JSON Web Keys response. + * The status code is non-200 and the response itself will fail. + * + * @return {Object} A nock response object. + */ +function mockFailedFetchJsonWebKeys(): nock.Scope { + return nock('https://firebaseappcheck.googleapis.com') + .get(jwksPath) + .replyWithError('message'); +} + +const TOKEN_PAYLOAD = { + one: 'uno', + 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: {} + }); + + 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: {} + }); + + return verifyJwtSignature(mockIdToken, '') + .should.eventually.be.fulfilled; + }); + + it('should be fulfilled given a valid signed token and a function to provide public keys', () => { + const mockIdToken = mocks.generateIdToken(); + const getKeyCallback = (_: any, callback: any): void => callback(null, mocks.keyPairs[0].public); + + return 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 fullfilled 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 fullfilled given a valid token without a kid (should check against all the keys)', () => { + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves({ 'kid-other': 'key-other', ...VALID_PUBLIC_KEYS_RESPONSE }); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken({ + header: {} + }); + + return verifier.verify(mockIdToken).should.eventually.be.fulfilled; + }); + + it('should be rejected given an expired token without a kid (should check against all the keys)', () => { + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves({ 'kid-other': 'key-other', ...VALID_PUBLIC_KEYS_RESPONSE }); + stubs.push(keyFetcherStub); + clock = sinon.useFakeTimers(1000); + const mockIdToken = mocks.generateIdToken({ + header: {} + }); + clock.tick((ONE_HOUR_IN_SECONDS * 1000) - 1); + + // token should still be valid + return verifier.verify(mockIdToken) + .then(() => { + clock!.tick(1); + + // token should now be invalid + return verifier.verify(mockIdToken).should.eventually.be.rejectedWith( + 'The provided token has expired. Get a fresh token from your client app and try again.') + .with.property('code', JwtErrorCode.TOKEN_EXPIRED); + }); + }); + + it('should be rejected given a token with an incorrect algorithm', () => { + const keyFetcherStub = sinon.stub(UrlKeyFetcher.prototype, 'fetchPublicKeys') + .resolves(VALID_PUBLIC_KEYS_RESPONSE); + stubs.push(keyFetcherStub); + const mockIdToken = mocks.generateIdToken({ + algorithm: 'HS256', + }); + + return verifier.verify(mockIdToken).should.eventually.be + .rejectedWith('invalid algorithm') + .with.property('code', 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 fullfilled given a valid unsigned (emulator) token', () => { + const mockIdToken = mocks.generateIdToken({ + algorithm: 'none', + header: {} + }); + + return emulatorVerifier.verify(mockIdToken).should.eventually.be.fulfilled; + }); + + it('should be rejected given a valid signed (non-emulator) token', () => { + const mockIdToken = mocks.generateIdToken(); + + return emulatorVerifier.verify(mockIdToken).should.eventually.be.rejected; + }); + }); +}); + +describe('UrlKeyFetcher', () => { + const agent = new https.Agent(); + let keyFetcher: 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((ONE_DAY_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledOnce; + clock!.tick(ONE_DAY_IN_SECONDS * 1000); // 24 hours in milliseconds + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // App check keys do not contain cache headers so we cache the keys for 24 hours. + // 24 hours has passed + expect(https.request).to.have.been.calledTwice; + clock!.tick((ONE_DAY_IN_SECONDS - 1) * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + expect(https.request).to.have.been.calledTwice; + clock!.tick(ONE_DAY_IN_SECONDS * 1000); + return keyFetcher.fetchPublicKeys(); + }).then(() => { + // 48 hours have passed + expect(https.request).to.have.been.calledThrice; + }); + }); + + it('should be rejected if fetching the public keys fails', () => { + mockedRequests.push(mockFailedFetchJsonWebKeys()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('message'); + }); + + it('should be rejected if fetching the public keys returns a response with an error message', () => { + mockedRequests.push(mockFetchJsonWebKeysWithErrorResponse()); + + return keyFetcher.fetchPublicKeys() + .should.eventually.be.rejectedWith('Error fetching Json Web Keys'); + }); + }); +}); diff --git a/test/unit/utils/validator.spec.ts b/test/unit/utils/validator.spec.ts old mode 100755 new mode 100644 index c8ab764f93..15d550c27d --- 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"); @@ -307,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', () => { @@ -355,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', () => { diff --git a/tsconfig.json b/tsconfig.json index e13292b408..67f91761f3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "compilerOptions": { "module": "commonjs", "target": "es5", + "declaration": true, + "sourceMap": true, "noImplicitAny": true, "noUnusedLocals": true, // TODO(rsgowman): enable `"strict": true,` and remove explicit setting of: noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, strictNullChecks, strictFunctionTypes, strictPropertyInitialization. @@ -11,10 +13,8 @@ "strictNullChecks": true, "strictFunctionTypes": true, //"strictPropertyInitialization": true, - "lib": ["es2015"], + "lib": ["es2018"], "outDir": "lib", - // We manually craft typings in src/index.d.ts instead of auto-generating them. - // "declaration": true, "rootDir": "." }, "files": [