diff --git a/.buildignore b/.buildignore index 227a589..1b02512 100644 --- a/.buildignore +++ b/.buildignore @@ -9,4 +9,4 @@ tsconfig.json .buildignore *.gz *.tar -yarn.lock \ No newline at end of file +yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f0de916 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig: https://EditorConfig.org + +[*] +charset = utf-8 +insert_final_newline = true # Type of newline is managed by git in .gitattributes +trim_trailing_whitespace = true + +[{*.{Dockerfile,css,js,jsx,ts,tsx},Dockerfile}] +indent_style = tab + +[*.{yml,yaml}] # YAML does not allow tab indentation +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ee1c4eb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Auto-detect text files +* text=auto + +# Always treat these as LF +*.sh text eol=lf + +# Always treat these as CRLF +*.bat text eol=crlf diff --git a/.github/workflows/code-formatting.yml b/.github/workflows/code-formatting.yml new file mode 100644 index 0000000..d7142ab --- /dev/null +++ b/.github/workflows/code-formatting.yml @@ -0,0 +1,26 @@ +# This name is shown in status badges +name: code-formatting + +on: + push: + branches-ignore: + - 'tmp**' + pull_request: + branches-ignore: + - 'tmp**' + +jobs: + editorconfig: + name: Check EditorConfig compliance + + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Set up editorconfig-checker + uses: editorconfig-checker/action-editorconfig-checker@v2 + + - name: Check code formatting + run: editorconfig-checker diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a664bb..e70c03c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "editor.tabSize": 2, "editor.detectIndentation": false, "editor.insertSpaces": false -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index 5d84c41..3c12780 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ WORKDIR /app COPY . . RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - apt-get update -y && apt-get install g++ python3 make -y && yarn cache clean && yarn install && yarn build + apt-get update -y && apt-get install g++ python3 make -y && yarn cache clean && yarn install && yarn build # Production stage FROM node:18-bullseye-slim AS production @@ -17,10 +17,10 @@ COPY --from=builder /app/public ./public RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - apt-get update -y && apt-get install g++ python3 make -y && yarn install --production + apt-get update -y && apt-get install g++ python3 make -y && yarn install --production ENV NODE_ENV production EXPOSE 8002 -CMD ["node", "./dist/src/app.js"] \ No newline at end of file +CMD ["node", "./dist/src/app.js"] diff --git a/README.md b/README.md index 8f950a9..23a4421 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ yarn install ## Change configuration Edit `config/config.dev.ts` file to change the configuration of the app. -## Run in dev mode +## Run in dev mode ``` yarn dev @@ -70,7 +70,7 @@ rsync --rsh='ssh -p 65432' .tar.gz root@ip:/tmp cd /tmp rm -rf wallet-backend mkdir wallet-backend -tar -xf .tar.gz -C wallet-backend +tar -xf .tar.gz -C wallet-backend cd wallet-backend chmod +x entrypoint.sh ./entrypoint.sh @@ -80,5 +80,3 @@ Add `Listen 9002` below the `Listen 443` line on `/etc/apache2/ports.conf` and restart apache # 3 Logging - - diff --git a/cli/.dockerignore b/cli/.dockerignore index 4122596..1ed8eaf 100644 --- a/cli/.dockerignore +++ b/cli/.dockerignore @@ -3,4 +3,4 @@ node_modules combined.log error.log *.gz -./dist \ No newline at end of file +./dist diff --git a/cli/Dockerfile b/cli/Dockerfile index 21c72b9..548ae73 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -7,4 +7,4 @@ COPY . . RUN yarn cache clean && yarn install -CMD ["tail", "-f", "/dev/null"] \ No newline at end of file +CMD ["tail", "-f", "/dev/null"] diff --git a/config/config.template.ts b/config/config.template.ts index 7eaa4c7..393acf0 100644 --- a/config/config.template.ts +++ b/config/config.template.ts @@ -28,4 +28,4 @@ export = { enabled: "NOTIFICATIONS_ENABLED", serviceAccount: "firebaseConfig.json" } -} \ No newline at end of file +} diff --git a/config/index.ts b/config/index.ts index 775fbd0..22ac3ed 100644 --- a/config/index.ts +++ b/config/index.ts @@ -2,4 +2,4 @@ require('dotenv').config() const env = process.env.NODE_ENV || 'development'; const config: any = require('./config.' + env); -export default config; \ No newline at end of file +export default config; diff --git a/development.Dockerfile b/development.Dockerfile index c55adcd..99c1d2a 100644 --- a/development.Dockerfile +++ b/development.Dockerfile @@ -5,7 +5,7 @@ WORKDIR /dependencies # Install dependencies first so rebuild of these layers is only needed when dependencies change COPY package.json yarn.lock ./ RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - yarn cache clean && yarn install + yarn cache clean && yarn install FROM node:16-bullseye-slim as cli-dependencies @@ -15,7 +15,7 @@ WORKDIR /dependencies # Install dependencies first so rebuild of these layers is only needed when dependencies change COPY cli/package.json cli/yarn.lock ./ RUN --mount=type=secret,id=npmrc,required=true,target=./.npmrc,uid=1000 \ - yarn cache clean && yarn install --frozen-lockfile + yarn cache clean && yarn install --frozen-lockfile FROM node:16-bullseye-slim as development diff --git a/nodemon.json b/nodemon.json index 4c0783e..dd48607 100644 --- a/nodemon.json +++ b/nodemon.json @@ -10,4 +10,4 @@ "." ], "ext": "ts,js" -} \ No newline at end of file +} diff --git a/public/alt-vc-logo.png b/public/alt-vc-logo.png index bc04d5d..e69de29 100644 Binary files a/public/alt-vc-logo.png and b/public/alt-vc-logo.png differ diff --git a/samples/wallet-mock/app.js b/samples/wallet-mock/app.js index 92fff73..657940d 100644 --- a/samples/wallet-mock/app.js +++ b/samples/wallet-mock/app.js @@ -77,7 +77,7 @@ app.get('/vp', async (req, res) => { const payload = JSON.parse(base64url.decode(vpjwt.split('.')[1])); return payload; }) - + res.render('presentations', { vp_list: vp_list @@ -121,7 +121,7 @@ app.get('/vp/:vp_id', async (req, res) => { console.dir(vp, { depth: null}) const vpjwt = vp.presentation; const payload = base64url.decode(vpjwt.split('.')[1]); - + res.render('vc', { title: "Wallet Mock", vc: payload @@ -143,7 +143,7 @@ app.get('/init/issuance/:iss', async (req, res) => { const selectedIssuerDID = iss == 'vid' ? vidTrustedIssuerDID : uoaTrustedIssuerDID; try { - const issuanceInitiation = await axios.post(walletBackendUrl + '/issuance/generate/authorization/request', + const issuanceInitiation = await axios.post(walletBackendUrl + '/issuance/generate/authorization/request', { legal_person_did: selectedIssuerDID }, { headers: { "Authorization": `Bearer ${global.user.appToken}` }} ); @@ -172,11 +172,11 @@ app.get('/init/verification/vid', async (req, res) => { /** - * For OpenID 4 VCI (Issuance) - * @param {*} req - * @param {*} res - * @param {*} next - */ +* For OpenID 4 VCI (Issuance) +* @param {*} req +* @param {*} res +* @param {*} next +*/ async function handleCredentialOffer(req, res, next) { const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; @@ -192,13 +192,13 @@ async function handleCredentialOffer(req, res, next) { } /** - * For OpenID 4 VCI (Issuance) - * @param {*} req - * @param {*} res - * @param {*} next - */ +* For OpenID 4 VCI (Issuance) +* @param {*} req +* @param {*} res +* @param {*} next +*/ async function handleAuthorizationResponse(req, res, next) { - const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; + const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; axios.post(walletBackendUrl + "/issuance/handle/authorization/response", { authorization_response_url: url }, @@ -214,13 +214,13 @@ async function handleAuthorizationResponse(req, res, next) { /** - * For OpenID 4 VP (Verification) - * @param {*} req - * @param {*} res - * @param {*} next - */ +* For OpenID 4 VP (Verification) +* @param {*} req +* @param {*} res +* @param {*} next +*/ async function handleAuthorizationRequest(req, res, next) { - const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; + const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; console.log("URL = ", url) axios.post(walletBackendUrl + "/presentation/handle/authorization/request", { authorization_request: url }, @@ -246,33 +246,33 @@ async function handleAuthorizationRequest(req, res, next) { app.post('/select-vc', async (req, res) => { console.log("Req = ", req.body) axios.post(walletBackendUrl + "/presentation/generate/authorization/response", - { verifiable_credentials_map: req.body }, - { headers: { "Authorization": `Bearer ${global.user.appToken}` }} - ).then(success => { - const { redirect_to } = success.data; - res.redirect(redirect_to); - }).catch(e => { - // console.error("Failed to generate authorization response") - // console.error(e.response.data); - res.render('error', { title: "Error", error: { status: 500 } }) - }); + { verifiable_credentials_map: req.body }, + { headers: { "Authorization": `Bearer ${global.user.appToken}` }} + ).then(success => { + const { redirect_to } = success.data; + res.redirect(redirect_to); + }).catch(e => { + // console.error("Failed to generate authorization response") + // console.error(e.response.data); + res.render('error', { title: "Error", error: { status: 500 } }) + }); }) // catch 404 and forward to error handler app.use(function(req, res, next) { - next(createError(404)); + next(createError(404)); }); // error handler app.use(function(err, req, res, next) { - // set locals, only providing error in development - res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; - // render the error page - res.status(err.status || 500); - res.render('error'); + // render the error page + res.status(err.status || 500); + res.render('error'); }); console.log("Started wallet mock server...") diff --git a/samples/wallet-mock/config.js b/samples/wallet-mock/config.js index ccc94b4..a77ac85 100644 --- a/samples/wallet-mock/config.js +++ b/samples/wallet-mock/config.js @@ -3,4 +3,4 @@ module.exports = { // trustedIssuerDID: "did:ebsi:zc6MhmU4NbKSAtAHx8XgpEW", uoaTrustedIssuerDID: "did:ebsi:zpq1XFkNWgsGB6MuvJp21vA", vidTrustedIssuerDID: "did:ebsi:zyhE5cJ7VVqYT4gZmoKadFt", -} \ No newline at end of file +} diff --git a/samples/wallet-mock/development.Dockerfile b/samples/wallet-mock/development.Dockerfile index 84ff434..3f0a1b7 100644 --- a/samples/wallet-mock/development.Dockerfile +++ b/samples/wallet-mock/development.Dockerfile @@ -17,4 +17,4 @@ ENV NODE_ENV development RUN chown -R node:node /home/node/app/node_modules USER node -CMD ["yarn", "dev"] \ No newline at end of file +CMD ["yarn", "dev"] diff --git a/samples/wallet-mock/lib.js b/samples/wallet-mock/lib.js index 8bad1ff..f9e87b8 100644 --- a/samples/wallet-mock/lib.js +++ b/samples/wallet-mock/lib.js @@ -21,4 +21,4 @@ async function registerUser() { module.exports = { registerUser -} \ No newline at end of file +} diff --git a/samples/wallet-mock/nodemon.json b/samples/wallet-mock/nodemon.json index 008adb4..8ef0dab 100644 --- a/samples/wallet-mock/nodemon.json +++ b/samples/wallet-mock/nodemon.json @@ -7,4 +7,4 @@ "." ], "ext": "js" -} \ No newline at end of file +} diff --git a/samples/wallet-mock/public/javascripts/index.js b/samples/wallet-mock/public/javascripts/index.js index 2582621..703590f 100644 --- a/samples/wallet-mock/public/javascripts/index.js +++ b/samples/wallet-mock/public/javascripts/index.js @@ -1,19 +1,19 @@ (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o', newValue); // }); - + view.expand(true); - + document.body.appendChild(view.dom); window.view = view; },{"json-view":7}],2:[function(require,module,exports){ @@ -37,23 +37,23 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. - + function EventEmitter() { this._events = this._events || {}; this._maxListeners = this._maxListeners || undefined; } module.exports = EventEmitter; - + // Backwards-compat with node 0.10.x EventEmitter.EventEmitter = EventEmitter; - + EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; - + // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; - + // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function(n) { @@ -62,13 +62,13 @@ this._maxListeners = n; return this; }; - + EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; - + if (!this._events) this._events = {}; - + // If there is no 'error' event listener then throw. if (type === 'error') { if (!this._events.error || @@ -84,12 +84,12 @@ } } } - + handler = this._events[type]; - + if (isUndefined(handler)) return false; - + if (isFunction(handler)) { switch (arguments.length) { // fast cases @@ -114,26 +114,26 @@ for (i = 0; i < len; i++) listeners[i].apply(this, args); } - + return true; }; - + EventEmitter.prototype.addListener = function(type, listener) { var m; - + if (!isFunction(listener)) throw TypeError('listener must be a function'); - + if (!this._events) this._events = {}; - + // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (this._events.newListener) this.emit('newListener', type, isFunction(listener.listener) ? listener.listener : listener); - + if (!this._events[type]) // Optimize the case of one listener. Don't need the extra array object. this._events[type] = listener; @@ -143,7 +143,7 @@ else // Adding the second element, need to change to array. this._events[type] = [this._events[type], listener]; - + // Check for listener leak if (isObject(this._events[type]) && !this._events[type].warned) { if (!isUndefined(this._maxListeners)) { @@ -151,7 +151,7 @@ } else { m = EventEmitter.defaultMaxListeners; } - + if (m && m > 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + @@ -164,53 +164,53 @@ } } } - + return this; }; - + EventEmitter.prototype.on = EventEmitter.prototype.addListener; - + EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); - + var fired = false; - + function g() { this.removeListener(type, g); - + if (!fired) { fired = true; listener.apply(this, arguments); } } - + g.listener = listener; this.on(type, g); - + return this; }; - + // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; - + if (!isFunction(listener)) throw TypeError('listener must be a function'); - + if (!this._events || !this._events[type]) return this; - + list = this._events[type]; length = list.length; position = -1; - + if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); - + } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || @@ -219,30 +219,30 @@ break; } } - + if (position < 0) return this; - + if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } - + if (this._events.removeListener) this.emit('removeListener', type, listener); } - + return this; }; - + EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; - + if (!this._events) return this; - + // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) @@ -251,7 +251,7 @@ delete this._events[type]; return this; } - + // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { @@ -262,9 +262,9 @@ this._events = {}; return this; } - + listeners = this._events[type]; - + if (isFunction(listeners)) { this.removeListener(type, listeners); } else if (listeners) { @@ -273,10 +273,10 @@ this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; - + return this; }; - + EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) @@ -287,11 +287,11 @@ ret = this._events[type].slice(); return ret; }; - + EventEmitter.prototype.listenerCount = function(type) { if (this._events) { var evlistener = this._events[type]; - + if (isFunction(evlistener)) return 1; else if (evlistener) @@ -299,39 +299,39 @@ } return 0; }; - + EventEmitter.listenerCount = function(emitter, type) { return emitter.listenerCount(type); }; - + function isFunction(arg) { return typeof arg === 'function'; } - + function isNumber(arg) { return typeof arg === 'number'; } - + function isObject(arg) { return typeof arg === 'object' && arg !== null; } - + function isUndefined(arg) { return arg === void 0; } - + },{}],3:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; - + // cached from whatever global is present so that test runners that stub it // don't break things. But we need to wrap it in a try catch in case it is // wrapped in strict mode code which doesn't define any globals. It's inside a // function because try/catches deoptimize in certain engines. - + var cachedSetTimeout; var cachedClearTimeout; - + function defaultSetTimout() { throw new Error('setTimeout has not been defined'); } @@ -380,8 +380,8 @@ return cachedSetTimeout.call(this, fun, 0); } } - - + + } function runClearTimeout(marker) { if (cachedClearTimeout === clearTimeout) { @@ -406,15 +406,15 @@ return cachedClearTimeout.call(this, marker); } } - - - + + + } var queue = []; var draining = false; var currentQueue; var queueIndex = -1; - + function cleanUpNextTick() { if (!draining || !currentQueue) { return; @@ -429,14 +429,14 @@ drainQueue(); } } - + function drainQueue() { if (draining) { return; } var timeout = runTimeout(cleanUpNextTick); draining = true; - + var len = queue.length; while(len) { currentQueue = queue; @@ -453,7 +453,7 @@ draining = false; runClearTimeout(timeout); } - + process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { @@ -466,7 +466,7 @@ runTimeout(drainQueue); } }; - + // v8 likes predictible objects function Item(fun, array) { this.fun = fun; @@ -481,9 +481,9 @@ process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; - + function noop() {} - + process.on = noop; process.addListener = noop; process.once = noop; @@ -491,17 +491,17 @@ process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; - + process.binding = function (name) { throw new Error('process.binding is not supported'); }; - + process.cwd = function () { return '/' }; process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; process.umask = function() { return 0; }; - + },{}],4:[function(require,module,exports){ if (typeof Object.create === 'function') { // implementation from standard node.js 'util' module @@ -526,7 +526,7 @@ ctor.prototype.constructor = ctor } } - + },{}],5:[function(require,module,exports){ module.exports = function isBuffer(arg) { return arg && typeof arg === 'object' @@ -556,7 +556,7 @@ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. - + var formatRegExp = /%[sdj%]/g; exports.format = function(f) { if (!isString(f)) { @@ -566,7 +566,7 @@ } return objects.join(' '); } - + var i = 1; var args = arguments; var len = args.length; @@ -595,8 +595,8 @@ } return str; }; - - + + // Mark that a method should not be used. // Returns a modified function which warns once by default. // If --no-deprecation is set, then it is a no-op. @@ -607,11 +607,11 @@ return exports.deprecate(fn, msg).apply(this, arguments); }; } - + if (process.noDeprecation === true) { return fn; } - + var warned = false; function deprecated() { if (!warned) { @@ -626,11 +626,11 @@ } return fn.apply(this, arguments); } - + return deprecated; }; - - + + var debugs = {}; var debugEnviron; exports.debuglog = function(set) { @@ -650,15 +650,15 @@ } return debugs[set]; }; - - + + /** - * Echos the value of a value. Trys to print the value out - * in the best way possible given the different types. - * - * @param {Object} obj The object to print out. - * @param {Object} opts Optional options object that alters the output. - */ + * Echos the value of a value. Trys to print the value out + * in the best way possible given the different types. + * + * @param {Object} obj The object to print out. + * @param {Object} opts Optional options object that alters the output. + */ /* legacy: obj, showHidden, depth, colors*/ function inspect(obj, opts) { // default options @@ -685,8 +685,8 @@ return formatValue(ctx, obj, ctx.depth); } exports.inspect = inspect; - - + + // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics inspect.colors = { 'bold' : [1, 22], @@ -703,7 +703,7 @@ 'red' : [31, 39], 'yellow' : [33, 39] }; - + // Don't use 'blue' not visible on cmd.exe inspect.styles = { 'special': 'cyan', @@ -716,36 +716,36 @@ // "name": intentionally not styling 'regexp': 'red' }; - - + + function stylizeWithColor(str, styleType) { var style = inspect.styles[styleType]; - + if (style) { return '\u001b[' + inspect.colors[style][0] + 'm' + str + - '\u001b[' + inspect.colors[style][1] + 'm'; + '\u001b[' + inspect.colors[style][1] + 'm'; } else { return str; } } - - + + function stylizeNoColor(str, styleType) { return str; } - - + + function arrayToHash(array) { var hash = {}; - + array.forEach(function(val, idx) { hash[val] = true; }); - + return hash; } - - + + function formatValue(ctx, value, recurseTimes) { // Provide a hook for user-specified inspect functions. // Check that value is an object with an inspect function on it @@ -762,28 +762,28 @@ } return ret; } - + // Primitive types cannot have properties var primitive = formatPrimitive(ctx, value); if (primitive) { return primitive; } - + // Look up the keys of the object. var keys = Object.keys(value); var visibleKeys = arrayToHash(keys); - + if (ctx.showHidden) { keys = Object.getOwnPropertyNames(value); } - + // IE doesn't make error fields non-enumerable // http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx if (isError(value) && (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) { return formatError(value); } - + // Some type of object without properties can be shortcutted. if (keys.length === 0) { if (isFunction(value)) { @@ -800,40 +800,40 @@ return formatError(value); } } - + var base = '', array = false, braces = ['{', '}']; - + // Make Array say that they are Array if (isArray(value)) { array = true; braces = ['[', ']']; } - + // Make functions say that they are functions if (isFunction(value)) { var n = value.name ? ': ' + value.name : ''; base = ' [Function' + n + ']'; } - + // Make RegExps say that they are RegExps if (isRegExp(value)) { base = ' ' + RegExp.prototype.toString.call(value); } - + // Make dates with properties first say the date if (isDate(value)) { base = ' ' + Date.prototype.toUTCString.call(value); } - + // Make error with message first say the error if (isError(value)) { base = ' ' + formatError(value); } - + if (keys.length === 0 && (!array || value.length == 0)) { return braces[0] + base + braces[1]; } - + if (recurseTimes < 0) { if (isRegExp(value)) { return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); @@ -841,9 +841,9 @@ return ctx.stylize('[Object]', 'special'); } } - + ctx.seen.push(value); - + var output; if (array) { output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); @@ -852,20 +852,20 @@ return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); }); } - + ctx.seen.pop(); - + return reduceToSingleString(output, base, braces); } - - + + function formatPrimitive(ctx, value) { if (isUndefined(value)) return ctx.stylize('undefined', 'undefined'); if (isString(value)) { var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"') + '\''; + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; return ctx.stylize(simple, 'string'); } if (isNumber(value)) @@ -876,13 +876,13 @@ if (isNull(value)) return ctx.stylize('null', 'null'); } - - + + function formatError(value) { return '[' + Error.prototype.toString.call(value) + ']'; } - - + + function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { var output = []; for (var i = 0, l = value.length; i < l; ++i) { @@ -901,8 +901,8 @@ }); return output; } - - + + function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { var name, str, desc; desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; @@ -952,16 +952,16 @@ name = ctx.stylize(name, 'name'); } else { name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/(^"|"$)/g, "'"); + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); name = ctx.stylize(name, 'string'); } } - + return name + ': ' + str; } - - + + function reduceToSingleString(output, base, braces) { var numLinesEst = 0; var length = output.reduce(function(prev, cur) { @@ -969,113 +969,113 @@ if (cur.indexOf('\n') >= 0) numLinesEst++; return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1; }, 0); - + if (length > 60) { return braces[0] + - (base === '' ? '' : base + '\n ') + - ' ' + - output.join(',\n ') + - ' ' + - braces[1]; + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; } - + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; } - - + + // NOTE: These type checking functions intentionally don't use `instanceof` // because it is fragile and can be easily faked with `Object.create()`. function isArray(ar) { return Array.isArray(ar); } exports.isArray = isArray; - + function isBoolean(arg) { return typeof arg === 'boolean'; } exports.isBoolean = isBoolean; - + function isNull(arg) { return arg === null; } exports.isNull = isNull; - + function isNullOrUndefined(arg) { return arg == null; } exports.isNullOrUndefined = isNullOrUndefined; - + function isNumber(arg) { return typeof arg === 'number'; } exports.isNumber = isNumber; - + function isString(arg) { return typeof arg === 'string'; } exports.isString = isString; - + function isSymbol(arg) { return typeof arg === 'symbol'; } exports.isSymbol = isSymbol; - + function isUndefined(arg) { return arg === void 0; } exports.isUndefined = isUndefined; - + function isRegExp(re) { return isObject(re) && objectToString(re) === '[object RegExp]'; } exports.isRegExp = isRegExp; - + function isObject(arg) { return typeof arg === 'object' && arg !== null; } exports.isObject = isObject; - + function isDate(d) { return isObject(d) && objectToString(d) === '[object Date]'; } exports.isDate = isDate; - + function isError(e) { return isObject(e) && (objectToString(e) === '[object Error]' || e instanceof Error); } exports.isError = isError; - + function isFunction(arg) { return typeof arg === 'function'; } exports.isFunction = isFunction; - + function isPrimitive(arg) { return arg === null || - typeof arg === 'boolean' || - typeof arg === 'number' || - typeof arg === 'string' || - typeof arg === 'symbol' || // ES6 symbol - typeof arg === 'undefined'; + typeof arg === 'boolean' || + typeof arg === 'number' || + typeof arg === 'string' || + typeof arg === 'symbol' || // ES6 symbol + typeof arg === 'undefined'; } exports.isPrimitive = isPrimitive; - + exports.isBuffer = require('./support/isBuffer'); - + function objectToString(o) { return Object.prototype.toString.call(o); } - - + + function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } - - + + var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - + // 26 Feb 16:19:34 function timestamp() { var d = new Date(); @@ -1084,14 +1084,14 @@ pad(d.getSeconds())].join(':'); return [d.getDate(), months[d.getMonth()], time].join(' '); } - - + + // log is just a thin wrapper to console.log that prepends a timestamp exports.log = function() { console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments)); }; - - + + /** * Inherit the prototype methods from one constructor into another. * @@ -1106,11 +1106,11 @@ * @param {function} superCtor Constructor function to inherit prototype from. */ exports.inherits = require('inherits'); - + exports._extend = function(origin, add) { // Don't do anything if add isn't an object if (!add || !isObject(add)) return origin; - + var keys = Object.keys(add); var i = keys.length; while (i--) { @@ -1118,42 +1118,42 @@ } return origin; }; - + function hasOwnProperty(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); } - + }).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) - + },{"./support/isBuffer":5,"_process":3,"inherits":4}],7:[function(require,module,exports){ /** * Created by richard.livingston on 18/02/2017. */ 'use strict'; - + var util = require('util'), EE = require('events').EventEmitter; - - + + module.exports = JSONView; util.inherits(JSONView, EE); - - + + function JSONView(name_, value_){ var self = this; - + EE.call(self); - + if(arguments.length < 2){ value_ = name_; name_ = undefined; } - + var name, value, type, domEventListeners = [], children = [], expanded = false, edittingName = false, edittingValue = false, nameEditable = true, valueEditable = true; - + var dom = { container : document.createElement('div'), collapseExpand : document.createElement('div'), @@ -1164,142 +1164,142 @@ children : document.createElement('div'), insert : document.createElement('div') }; - - + + Object.defineProperties(self, { - + dom : { value : dom.container, enumerable : true }, - + name : { get : function(){ return name; }, - + set : setName, enumerable : true }, - + value : { get : function(){ return value; }, - + set : setValue, enumerable : true }, - + type : { get : function(){ return type; }, - + enumerable : true }, - + nameEditable : { get : function(){ return nameEditable; }, - + set : function(value){ nameEditable = !!value; }, - + enumerable : true }, - + valueEditable : { get : function(){ return valueEditable; }, - + set : function(value){ valueEditable = !!value; }, - + enumerable : true }, - + refresh : { value : refresh, enumerable : true }, - + collapse : { value : collapse, enumerable : true }, - + expand : { value : expand, enumerable : true }, - + destroy : { value : destroy, enumerable : true }, - + editName : { value : editField.bind(null, 'name'), enumerable : true }, - + editValue : { value : editField.bind(null, 'value'), enumerable : true } - + }); - - + + Object.keys(dom).forEach(function(k){ var element = dom[k]; - + if(k == 'container'){ return; } - + element.className = k; dom.container.appendChild(element); }); - + dom.container.className = 'jsonView'; - + addDomEventListener(dom.collapseExpand, 'click', onCollapseExpandClick); addDomEventListener(dom.value, 'click', expand.bind(null, false)); addDomEventListener(dom.name, 'click', expand.bind(null, false)); - + addDomEventListener(dom.name, 'dblclick', editField.bind(null, 'name')); addDomEventListener(dom.name, 'blur', editFieldStop.bind(null, 'name')); addDomEventListener(dom.name, 'keypress', editFieldKeyPressed.bind(null, 'name')); addDomEventListener(dom.name, 'keydown', editFieldTabPressed.bind(null, 'name')); - + addDomEventListener(dom.value, 'dblclick', editField.bind(null, 'value')); addDomEventListener(dom.value, 'blur', editFieldStop.bind(null, 'value')); addDomEventListener(dom.value, 'keypress', editFieldKeyPressed.bind(null, 'value')); addDomEventListener(dom.value, 'keydown', editFieldTabPressed.bind(null, 'value')); addDomEventListener(dom.value, 'keydown', numericValueKeyDown); - + addDomEventListener(dom.insert, 'click', onInsertClick); addDomEventListener(dom.delete, 'click', onDeleteClick); - + setName(name_); setValue(value_); - - + + function refresh(){ var expandable = type == 'object' || type == 'array'; - + children.forEach(function(child){ child.refresh(); }); - + dom.collapseExpand.style.display = expandable ? '' : 'none'; - + if(expanded && expandable){ expand(); } @@ -1307,27 +1307,27 @@ collapse(); } } - - + + function collapse(recursive){ if(recursive){ children.forEach(function(child){ child.collapse(true); }); } - + expanded = false; - + dom.children.style.display = 'none'; dom.collapseExpand.className = 'expand'; dom.container.classList.add('collapsed'); dom.container.classList.remove('expanded'); } - - + + function expand(recursive){ var keys; - + if(type == 'object'){ keys = Object.keys(value); } @@ -1339,76 +1339,76 @@ else{ keys = []; } - + // Remove children that no longer exist for(var i = children.length - 1; i >= 0; i --){ var child = children[i]; - + if(keys.indexOf(child.name) == -1){ children.splice(i, 1); removeChild(child); } } - + if(type != 'object' && type != 'array'){ return collapse(); } - + keys.forEach(function(key){ addChild(key, value[key]); }); - + if(recursive){ children.forEach(function(child){ child.expand(true); }); } - + expanded = true; dom.children.style.display = ''; dom.collapseExpand.className = 'collapse'; dom.container.classList.add('expanded'); dom.container.classList.remove('collapsed'); } - - + + function destroy(){ var child, event; - + while(event = domEventListeners.pop()){ event.element.removeEventListener(event.name, event.fn); } - + while(child = children.pop()){ removeChild(child); } } - - + + function setName(newName){ var nameType = typeof newName, oldName = name; - + if(newName === name){ return; } - + if(nameType != 'string' && nameType != 'number'){ throw new Error('Name must be either string or number, ' + newName); } - + dom.name.innerText = newName; name = newName; self.emit('rename', self, oldName, newName); } - - + + function setValue(newValue){ var oldValue = value, str; - + type = getType(newValue); - + switch(type){ case 'null': str = 'null'; @@ -1416,54 +1416,54 @@ case 'object': str = 'Object[' + Object.keys(newValue).length + ']'; break; - + case 'array': str = 'Array[' + newValue.length + ']'; break; - + default: str = newValue; break; } - + dom.value.innerText = str; dom.value.className = 'value ' + type; - + if(newValue === value){ return; } - + value = newValue; - + if(type == 'array' || type == 'object'){ // Cannot edit objects as string because the formatting is too messy // Would have to either pass as JSON and force user to wrap properties in quotes // Or first JSON stringify the input before passing, this could allow users to reference globals - + // Instead the user can modify individual properties, or just delete the object and start again valueEditable = false; - + if(type == 'array'){ // Obviously cannot modify array keys nameEditable = false; } } - + refresh(); self.emit('change', name, oldValue, newValue); } - - + + function addChild(key, val){ var child; - + for(var i = 0, len = children.length; i < len; i ++){ if(children[i].name == key){ child = children[i]; break; } } - + if(child){ child.value = val; } @@ -1474,67 +1474,67 @@ child.on('change', onChildChange); children.push(child); } - + dom.children.appendChild(child.dom); - + return child; } - - + + function removeChild(child){ if(child.dom.parentNode){ dom.children.removeChild(child.dom); } - + child.destroy(); child.removeAllListeners(); } - - + + function editField(field){ var editable = field == 'name' ? nameEditable : valueEditable, element = dom[field]; - + if(!editable){ return; } - + if(field == 'value' && type == 'string'){ element.innerText = '"' + value + '"'; } - + if(field == 'name'){ edittingName = true; } - + if(field == 'value'){ edittingValue = true; } - + element.classList.add('edit'); element.setAttribute('contenteditable', true); element.focus(); document.execCommand('selectAll', false, null); } - - + + function editFieldStop(field){ var element = dom[field]; - + if(field == 'name'){ if(!edittingName){ return; } edittingName = false; } - + if(field == 'value'){ if(!edittingValue){ return; } edittingValue = false; } - + if(field == 'name'){ setName(element.innerText); } @@ -1546,12 +1546,12 @@ setValue(element.innerText); } } - + element.classList.remove('edit'); element.removeAttribute('contenteditable'); } - - + + function editFieldKeyPressed(field, e){ switch(e.key){ case 'Escape': @@ -1560,12 +1560,12 @@ break; } } - - + + function editFieldTabPressed(field, e){ if(e.key == 'Tab'){ editFieldStop(field); - + if(field == 'name'){ e.preventDefault(); editField('value'); @@ -1575,62 +1575,62 @@ } } } - - + + function numericValueKeyDown(e){ var increment = 0, currentValue; - + if(type != 'number'){ return; } - + switch(e.key){ case 'ArrowDown': case 'Down': increment = -1; break; - + case 'ArrowUp': case 'Up': increment = 1; break; } - + if(e.shiftKey){ increment *= 10; } - + if(e.ctrlKey || e.metaKey){ increment /= 10; } - + if(increment){ currentValue = parseFloat(dom.value.innerText); - + if(!isNaN(currentValue)){ dom.value.innerText = Number((currentValue + increment).toFixed(10)); } } } - - + + function getType(value){ var type = typeof value; - + if(type == 'object'){ if(value === null){ return 'null'; } - + if(Array.isArray(value)){ return 'array'; } } - + return type; } - - + + function onCollapseExpandClick(){ if(expanded){ collapse(); @@ -1639,12 +1639,12 @@ expand(); } } - - + + function onInsertClick(){ var newName = type == 'array' ? value.length : undefined, child = addChild(newName, null); - + if(type == 'array'){ value.push(null); child.editValue(); @@ -1653,16 +1653,16 @@ child.editName(); } } - - + + function onDeleteClick(){ self.emit('delete', self); } - - + + function onChildRename(child, oldName, newName){ var allow = newName && type != 'array' && !(newName in value); - + if(allow){ value[newName] = child.value; delete value[oldName]; @@ -1675,38 +1675,38 @@ // Cannot rename array keys, or duplicate object key names child.name = oldName; } - + child.once('rename', onChildRename); } - - + + function onChildChange(keyPath, oldValue, newValue, recursed){ if(!recursed){ value[keyPath] = newValue; } - + self.emit('change', name + '.' + keyPath, oldValue, newValue, true); } - - + + function onChildDelete(child){ var key = child.name; - + if(type == 'array'){ value.splice(key, 1); } else{ delete value[key]; } - + refresh(); } - - + + function addDomEventListener(element, name, fn){ element.addEventListener(name, fn); domEventListeners.push({element : element, name : name, fn : fn}); } } },{"events":2,"util":6}]},{},[1]) - //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJpbmRleC5qcyIsIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9ldmVudHMvZXZlbnRzLmpzIiwibm9kZV9tb2R1bGVzL2Jyb3dzZXJpZnkvbm9kZV9tb2R1bGVzL3Byb2Nlc3MvYnJvd3Nlci5qcyIsIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy91dGlsL25vZGVfbW9kdWxlcy9pbmhlcml0cy9pbmhlcml0c19icm93c2VyLmpzIiwibm9kZV9tb2R1bGVzL2Jyb3dzZXJpZnkvbm9kZV9tb2R1bGVzL3V0aWwvc3VwcG9ydC9pc0J1ZmZlckJyb3dzZXIuanMiLCJub2RlX21vZHVsZXMvYnJvd3NlcmlmeS9ub2RlX21vZHVsZXMvdXRpbC91dGlsLmpzIiwibm9kZV9tb2R1bGVzL2pzb24tdmlldy9KU09OVmlldy5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQ0FBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3hCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDOVNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3BMQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDdkJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7O0FDTEE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQzFrQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIi8qKlxyXG4gKiBDcmVhdGVkIGJ5IHIxY2g0IG9uIDAyLzEwLzIwMTYuXHJcbiAqL1xyXG5cclxudmFyIEpTT05WaWV3ID0gcmVxdWlyZSgnanNvbi12aWV3Jyk7XHJcblxyXG52YXIgdmlldyA9IG5ldyBKU09OVmlldygnZXhhbXBsZScsIHtcclxuICAgIGhlbGxvIDogJ3dvcmxkJyxcclxuICAgIGRvdWJsZUNsaWNrIDogJ21lIHRvIGVkaXQnLFxyXG4gICAgYSA6IG51bGwsXHJcbiAgICBiIDogdHJ1ZSxcclxuICAgIGMgOiBmYWxzZSxcclxuICAgIGQgOiAxLFxyXG4gICAgZSA6IHtuZXN0ZWQgOiAnb2JqZWN0J30sXHJcbiAgICBmIDogWzEsMiwzXVxyXG59KTtcclxuXHJcbnZpZXcub24oJ2NoYW5nZScsIGZ1bmN0aW9uKGtleSwgb2xkVmFsdWUsIG5ld1ZhbHVlKXtcclxuICAgIGNvbnNvbGUubG9nKCdjaGFuZ2UnLCBrZXksIG9sZFZhbHVlLCAnPT4nLCBuZXdWYWx1ZSk7XHJcbn0pO1xyXG5cclxudmlldy5leHBhbmQodHJ1ZSk7XHJcblxyXG5kb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHZpZXcuZG9tKTtcclxud2luZG93LnZpZXcgPSB2aWV3OyIsIi8vIENvcHlyaWdodCBKb3llbnQsIEluYy4gYW5kIG90aGVyIE5vZGUgY29udHJpYnV0b3JzLlxuLy9cbi8vIFBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhXG4vLyBjb3B5IG9mIHRoaXMgc29mdHdhcmUgYW5kIGFzc29jaWF0ZWQgZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlXG4vLyBcIlNvZnR3YXJlXCIpLCB0byBkZWFsIGluIHRoZSBTb2Z0d2FyZSB3aXRob3V0IHJlc3RyaWN0aW9uLCBpbmNsdWRpbmdcbi8vIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCxcbi8vIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXRcbi8vIHBlcnNvbnMgdG8gd2hvbSB0aGUgU29mdHdhcmUgaXMgZnVybmlzaGVkIHRvIGRvIHNvLCBzdWJqZWN0IHRvIHRoZVxuLy8gZm9sbG93aW5nIGNvbmRpdGlvbnM6XG4vL1xuLy8gVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWRcbi8vIGluIGFsbCBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLlxuLy9cbi8vIFRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCBcIkFTIElTXCIsIFdJVEhPVVQgV0FSUkFOVFkgT0YgQU5ZIEtJTkQsIEVYUFJFU1Ncbi8vIE9SIElNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0Zcbi8vIE1FUkNIQU5UQUJJTElUWSwgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU5cbi8vIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1JTIE9SIENPUFlSSUdIVCBIT0xERVJTIEJFIExJQUJMRSBGT1IgQU5ZIENMQUlNLFxuLy8gREFNQUdFUyBPUiBPVEhFUiBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SXG4vLyBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFXG4vLyBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4gVEhFIFNPRlRXQVJFLlxuXG5mdW5jdGlvbiBFdmVudEVtaXR0ZXIoKSB7XG4gIHRoaXMuX2V2ZW50cyA9IHRoaXMuX2V2ZW50cyB8fCB7fTtcbiAgdGhpcy5fbWF4TGlzdGVuZXJzID0gdGhpcy5fbWF4TGlzdGVuZXJzIHx8IHVuZGVmaW5lZDtcbn1cbm1vZHVsZS5leHBvcnRzID0gRXZlbnRFbWl0dGVyO1xuXG4vLyBCYWNrd2FyZHMtY29tcGF0IHdpdGggbm9kZSAwLjEwLnhcbkV2ZW50RW1pdHRlci5FdmVudEVtaXR0ZXIgPSBFdmVudEVtaXR0ZXI7XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX2V2ZW50cyA9IHVuZGVmaW5lZDtcbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX21heExpc3RlbmVycyA9IHVuZGVmaW5lZDtcblxuLy8gQnkgZGVmYXVsdCBFdmVudEVtaXR0ZXJzIHdpbGwgcHJpbnQgYSB3YXJuaW5nIGlmIG1vcmUgdGhhbiAxMCBsaXN0ZW5lcnMgYXJlXG4vLyBhZGRlZCB0byBpdC4gVGhpcyBpcyBhIHVzZWZ1bCBkZWZhdWx0IHdoaWNoIGhlbHBzIGZpbmRpbmcgbWVtb3J5IGxlYWtzLlxuRXZlbnRFbWl0dGVyLmRlZmF1bHRNYXhMaXN0ZW5lcnMgPSAxMDtcblxuLy8gT2J2aW91c2x5IG5vdCBhbGwgRW1pdHRlcnMgc2hvdWxkIGJlIGxpbWl0ZWQgdG8gMTAuIFRoaXMgZnVuY3Rpb24gYWxsb3dzXG4vLyB0aGF0IHRvIGJlIGluY3JlYXNlZC4gU2V0IHRvIHplcm8gZm9yIHVubGltaXRlZC5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuc2V0TWF4TGlzdGVuZXJzID0gZnVuY3Rpb24obikge1xuICBpZiAoIWlzTnVtYmVyKG4pIHx8IG4gPCAwIHx8IGlzTmFOKG4pKVxuICAgIHRocm93IFR5cGVFcnJvcignbiBtdXN0IGJlIGEgcG9zaXRpdmUgbnVtYmVyJyk7XG4gIHRoaXMuX21heExpc3RlbmVycyA9IG47XG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5lbWl0ID0gZnVuY3Rpb24odHlwZSkge1xuICB2YXIgZXIsIGhhbmRsZXIsIGxlbiwgYXJncywgaSwgbGlzdGVuZXJzO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzKVxuICAgIHRoaXMuX2V2ZW50cyA9IHt9O1xuXG4gIC8vIElmIHRoZXJlIGlzIG5vICdlcnJvcicgZXZlbnQgbGlzdGVuZXIgdGhlbiB0aHJvdy5cbiAgaWYgKHR5cGUgPT09ICdlcnJvcicpIHtcbiAgICBpZiAoIXRoaXMuX2V2ZW50cy5lcnJvciB8fFxuICAgICAgICAoaXNPYmplY3QodGhpcy5fZXZlbnRzLmVycm9yKSAmJiAhdGhpcy5fZXZlbnRzLmVycm9yLmxlbmd0aCkpIHtcbiAgICAgIGVyID0gYXJndW1lbnRzWzFdO1xuICAgICAgaWYgKGVyIGluc3RhbmNlb2YgRXJyb3IpIHtcbiAgICAgICAgdGhyb3cgZXI7IC8vIFVuaGFuZGxlZCAnZXJyb3InIGV2ZW50XG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBBdCBsZWFzdCBnaXZlIHNvbWUga2luZCBvZiBjb250ZXh0IHRvIHRoZSB1c2VyXG4gICAgICAgIHZhciBlcnIgPSBuZXcgRXJyb3IoJ1VuY2F1Z2h0LCB1bnNwZWNpZmllZCBcImVycm9yXCIgZXZlbnQuICgnICsgZXIgKyAnKScpO1xuICAgICAgICBlcnIuY29udGV4dCA9IGVyO1xuICAgICAgICB0aHJvdyBlcnI7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgaGFuZGxlciA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcblxuICBpZiAoaXNVbmRlZmluZWQoaGFuZGxlcikpXG4gICAgcmV0dXJuIGZhbHNlO1xuXG4gIGlmIChpc0Z1bmN0aW9uKGhhbmRsZXIpKSB7XG4gICAgc3dpdGNoIChhcmd1bWVudHMubGVuZ3RoKSB7XG4gICAgICAvLyBmYXN0IGNhc2VzXG4gICAgICBjYXNlIDE6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlIDI6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzLCBhcmd1bWVudHNbMV0pO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgMzpcbiAgICAgICAgaGFuZGxlci5jYWxsKHRoaXMsIGFyZ3VtZW50c1sxXSwgYXJndW1lbnRzWzJdKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICAvLyBzbG93ZXJcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIGFyZ3MgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuICAgICAgICBoYW5kbGVyLmFwcGx5KHRoaXMsIGFyZ3MpO1xuICAgIH1cbiAgfSBlbHNlIGlmIChpc09iamVjdChoYW5kbGVyKSkge1xuICAgIGFyZ3MgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuICAgIGxpc3RlbmVycyA9IGhhbmRsZXIuc2xpY2UoKTtcbiAgICBsZW4gPSBsaXN0ZW5lcnMubGVuZ3RoO1xuICAgIGZvciAoaSA9IDA7IGkgPCBsZW47IGkrKylcbiAgICAgIGxpc3RlbmVyc1tpXS5hcHBseSh0aGlzLCBhcmdzKTtcbiAgfVxuXG4gIHJldHVybiB0cnVlO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5hZGRMaXN0ZW5lciA9IGZ1bmN0aW9uKHR5cGUsIGxpc3RlbmVyKSB7XG4gIHZhciBtO1xuXG4gIGlmICghaXNGdW5jdGlvbihsaXN0ZW5lcikpXG4gICAgdGhyb3cgVHlwZUVycm9yKCdsaXN0ZW5lciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcblxuICBpZiAoIXRoaXMuX2V2ZW50cylcbiAgICB0aGlzLl9ldmVudHMgPSB7fTtcblxuICAvLyBUbyBhdm9pZCByZWN1cnNpb24gaW4gdGhlIGNhc2UgdGhhdCB0eXBlID09PSBcIm5ld0xpc3RlbmVyXCIhIEJlZm9yZVxuICAvLyBhZGRpbmcgaXQgdG8gdGhlIGxpc3RlbmVycywgZmlyc3QgZW1pdCBcIm5ld0xpc3RlbmVyXCIuXG4gIGlmICh0aGlzLl9ldmVudHMubmV3TGlzdGVuZXIpXG4gICAgdGhpcy5lbWl0KCduZXdMaXN0ZW5lcicsIHR5cGUsXG4gICAgICAgICAgICAgIGlzRnVuY3Rpb24obGlzdGVuZXIubGlzdGVuZXIpID9cbiAgICAgICAgICAgICAgbGlzdGVuZXIubGlzdGVuZXIgOiBsaXN0ZW5lcik7XG5cbiAgaWYgKCF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgLy8gT3B0aW1pemUgdGhlIGNhc2Ugb2Ygb25lIGxpc3RlbmVyLiBEb24ndCBuZWVkIHRoZSBleHRyYSBhcnJheSBvYmplY3QuXG4gICAgdGhpcy5fZXZlbnRzW3R5cGVdID0gbGlzdGVuZXI7XG4gIGVsc2UgaWYgKGlzT2JqZWN0KHRoaXMuX2V2ZW50c1t0eXBlXSkpXG4gICAgLy8gSWYgd2UndmUgYWxyZWFkeSBnb3QgYW4gYXJyYXksIGp1c3QgYXBwZW5kLlxuICAgIHRoaXMuX2V2ZW50c1t0eXBlXS5wdXNoKGxpc3RlbmVyKTtcbiAgZWxzZVxuICAgIC8vIEFkZGluZyB0aGUgc2Vjb25kIGVsZW1lbnQsIG5lZWQgdG8gY2hhbmdlIHRvIGFycmF5LlxuICAgIHRoaXMuX2V2ZW50c1t0eXBlXSA9IFt0aGlzLl9ldmVudHNbdHlwZV0sIGxpc3RlbmVyXTtcblxuICAvLyBDaGVjayBmb3IgbGlzdGVuZXIgbGVha1xuICBpZiAoaXNPYmplY3QodGhpcy5fZXZlbnRzW3R5cGVdKSAmJiAhdGhpcy5fZXZlbnRzW3R5cGVdLndhcm5lZCkge1xuICAgIGlmICghaXNVbmRlZmluZWQodGhpcy5fbWF4TGlzdGVuZXJzKSkge1xuICAgICAgbSA9IHRoaXMuX21heExpc3RlbmVycztcbiAgICB9IGVsc2Uge1xuICAgICAgbSA9IEV2ZW50RW1pdHRlci5kZWZhdWx0TWF4TGlzdGVuZXJzO1xuICAgIH1cblxuICAgIGlmIChtICYmIG0gPiAwICYmIHRoaXMuX2V2ZW50c1t0eXBlXS5sZW5ndGggPiBtKSB7XG4gICAgICB0aGlzLl9ldmVudHNbdHlwZV0ud2FybmVkID0gdHJ1ZTtcbiAgICAgIGNvbnNvbGUuZXJyb3IoJyhub2RlKSB3YXJuaW5nOiBwb3NzaWJsZSBFdmVudEVtaXR0ZXIgbWVtb3J5ICcgK1xuICAgICAgICAgICAgICAgICAgICAnbGVhayBkZXRlY3RlZC4gJWQgbGlzdGVuZXJzIGFkZGVkLiAnICtcbiAgICAgICAgICAgICAgICAgICAgJ1VzZSBlbWl0dGVyLnNldE1heExpc3RlbmVycygpIHRvIGluY3JlYXNlIGxpbWl0LicsXG4gICAgICAgICAgICAgICAgICAgIHRoaXMuX2V2ZW50c1t0eXBlXS5sZW5ndGgpO1xuICAgICAgaWYgKHR5cGVvZiBjb25zb2xlLnRyYWNlID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIC8vIG5vdCBzdXBwb3J0ZWQgaW4gSUUgMTBcbiAgICAgICAgY29uc29sZS50cmFjZSgpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5vbiA9IEV2ZW50RW1pdHRlci5wcm90b3R5cGUuYWRkTGlzdGVuZXI7XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUub25jZSA9IGZ1bmN0aW9uKHR5cGUsIGxpc3RlbmVyKSB7XG4gIGlmICghaXNGdW5jdGlvbihsaXN0ZW5lcikpXG4gICAgdGhyb3cgVHlwZUVycm9yKCdsaXN0ZW5lciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcblxuICB2YXIgZmlyZWQgPSBmYWxzZTtcblxuICBmdW5jdGlvbiBnKCkge1xuICAgIHRoaXMucmVtb3ZlTGlzdGVuZXIodHlwZSwgZyk7XG5cbiAgICBpZiAoIWZpcmVkKSB7XG4gICAgICBmaXJlZCA9IHRydWU7XG4gICAgICBsaXN0ZW5lci5hcHBseSh0aGlzLCBhcmd1bWVudHMpO1xuICAgIH1cbiAgfVxuXG4gIGcubGlzdGVuZXIgPSBsaXN0ZW5lcjtcbiAgdGhpcy5vbih0eXBlLCBnKTtcblxuICByZXR1cm4gdGhpcztcbn07XG5cbi8vIGVtaXRzIGEgJ3JlbW92ZUxpc3RlbmVyJyBldmVudCBpZmYgdGhlIGxpc3RlbmVyIHdhcyByZW1vdmVkXG5FdmVudEVtaXR0ZXIucHJvdG90eXBlLnJlbW92ZUxpc3RlbmVyID0gZnVuY3Rpb24odHlwZSwgbGlzdGVuZXIpIHtcbiAgdmFyIGxpc3QsIHBvc2l0aW9uLCBsZW5ndGgsIGk7XG5cbiAgaWYgKCFpc0Z1bmN0aW9uKGxpc3RlbmVyKSlcbiAgICB0aHJvdyBUeXBlRXJyb3IoJ2xpc3RlbmVyIG11c3QgYmUgYSBmdW5jdGlvbicpO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzIHx8ICF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgcmV0dXJuIHRoaXM7XG5cbiAgbGlzdCA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcbiAgbGVuZ3RoID0gbGlzdC5sZW5ndGg7XG4gIHBvc2l0aW9uID0gLTE7XG5cbiAgaWYgKGxpc3QgPT09IGxpc3RlbmVyIHx8XG4gICAgICAoaXNGdW5jdGlvbihsaXN0Lmxpc3RlbmVyKSAmJiBsaXN0Lmxpc3RlbmVyID09PSBsaXN0ZW5lcikpIHtcbiAgICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuICAgIGlmICh0aGlzLl9ldmVudHMucmVtb3ZlTGlzdGVuZXIpXG4gICAgICB0aGlzLmVtaXQoJ3JlbW92ZUxpc3RlbmVyJywgdHlwZSwgbGlzdGVuZXIpO1xuXG4gIH0gZWxzZSBpZiAoaXNPYmplY3QobGlzdCkpIHtcbiAgICBmb3IgKGkgPSBsZW5ndGg7IGktLSA+IDA7KSB7XG4gICAgICBpZiAobGlzdFtpXSA9PT0gbGlzdGVuZXIgfHxcbiAgICAgICAgICAobGlzdFtpXS5saXN0ZW5lciAmJiBsaXN0W2ldLmxpc3RlbmVyID09PSBsaXN0ZW5lcikpIHtcbiAgICAgICAgcG9zaXRpb24gPSBpO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAocG9zaXRpb24gPCAwKVxuICAgICAgcmV0dXJuIHRoaXM7XG5cbiAgICBpZiAobGlzdC5sZW5ndGggPT09IDEpIHtcbiAgICAgIGxpc3QubGVuZ3RoID0gMDtcbiAgICAgIGRlbGV0ZSB0aGlzLl9ldmVudHNbdHlwZV07XG4gICAgfSBlbHNlIHtcbiAgICAgIGxpc3Quc3BsaWNlKHBvc2l0aW9uLCAxKTtcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fZXZlbnRzLnJlbW92ZUxpc3RlbmVyKVxuICAgICAgdGhpcy5lbWl0KCdyZW1vdmVMaXN0ZW5lcicsIHR5cGUsIGxpc3RlbmVyKTtcbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5yZW1vdmVBbGxMaXN0ZW5lcnMgPSBmdW5jdGlvbih0eXBlKSB7XG4gIHZhciBrZXksIGxpc3RlbmVycztcblxuICBpZiAoIXRoaXMuX2V2ZW50cylcbiAgICByZXR1cm4gdGhpcztcblxuICAvLyBub3QgbGlzdGVuaW5nIGZvciByZW1vdmVMaXN0ZW5lciwgbm8gbmVlZCB0byBlbWl0XG4gIGlmICghdGhpcy5fZXZlbnRzLnJlbW92ZUxpc3RlbmVyKSB7XG4gICAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPT09IDApXG4gICAgICB0aGlzLl9ldmVudHMgPSB7fTtcbiAgICBlbHNlIGlmICh0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgLy8gZW1pdCByZW1vdmVMaXN0ZW5lciBmb3IgYWxsIGxpc3RlbmVycyBvbiBhbGwgZXZlbnRzXG4gIGlmIChhcmd1bWVudHMubGVuZ3RoID09PSAwKSB7XG4gICAgZm9yIChrZXkgaW4gdGhpcy5fZXZlbnRzKSB7XG4gICAgICBpZiAoa2V5ID09PSAncmVtb3ZlTGlzdGVuZXInKSBjb250aW51ZTtcbiAgICAgIHRoaXMucmVtb3ZlQWxsTGlzdGVuZXJzKGtleSk7XG4gICAgfVxuICAgIHRoaXMucmVtb3ZlQWxsTGlzdGVuZXJzKCdyZW1vdmVMaXN0ZW5lcicpO1xuICAgIHRoaXMuX2V2ZW50cyA9IHt9O1xuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgbGlzdGVuZXJzID0gdGhpcy5fZXZlbnRzW3R5cGVdO1xuXG4gIGlmIChpc0Z1bmN0aW9uKGxpc3RlbmVycykpIHtcbiAgICB0aGlzLnJlbW92ZUxpc3RlbmVyKHR5cGUsIGxpc3RlbmVycyk7XG4gIH0gZWxzZSBpZiAobGlzdGVuZXJzKSB7XG4gICAgLy8gTElGTyBvcmRlclxuICAgIHdoaWxlIChsaXN0ZW5lcnMubGVuZ3RoKVxuICAgICAgdGhpcy5yZW1vdmVMaXN0ZW5lcih0eXBlLCBsaXN0ZW5lcnNbbGlzdGVuZXJzLmxlbmd0aCAtIDFdKTtcbiAgfVxuICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5saXN0ZW5lcnMgPSBmdW5jdGlvbih0eXBlKSB7XG4gIHZhciByZXQ7XG4gIGlmICghdGhpcy5fZXZlbnRzIHx8ICF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgcmV0ID0gW107XG4gIGVsc2UgaWYgKGlzRnVuY3Rpb24odGhpcy5fZXZlbnRzW3R5cGVdKSlcbiAgICByZXQgPSBbdGhpcy5fZXZlbnRzW3R5cGVdXTtcbiAgZWxzZVxuICAgIHJldCA9IHRoaXMuX2V2ZW50c1t0eXBlXS5zbGljZSgpO1xuICByZXR1cm4gcmV0O1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5saXN0ZW5lckNvdW50ID0gZnVuY3Rpb24odHlwZSkge1xuICBpZiAodGhpcy5fZXZlbnRzKSB7XG4gICAgdmFyIGV2bGlzdGVuZXIgPSB0aGlzLl9ldmVudHNbdHlwZV07XG5cbiAgICBpZiAoaXNGdW5jdGlvbihldmxpc3RlbmVyKSlcbiAgICAgIHJldHVybiAxO1xuICAgIGVsc2UgaWYgKGV2bGlzdGVuZXIpXG4gICAgICByZXR1cm4gZXZsaXN0ZW5lci5sZW5ndGg7XG4gIH1cbiAgcmV0dXJuIDA7XG59O1xuXG5FdmVudEVtaXR0ZXIubGlzdGVuZXJDb3VudCA9IGZ1bmN0aW9uKGVtaXR0ZXIsIHR5cGUpIHtcbiAgcmV0dXJuIGVtaXR0ZXIubGlzdGVuZXJDb3VudCh0eXBlKTtcbn07XG5cbmZ1bmN0aW9uIGlzRnVuY3Rpb24oYXJnKSB7XG4gIHJldHVybiB0eXBlb2YgYXJnID09PSAnZnVuY3Rpb24nO1xufVxuXG5mdW5jdGlvbiBpc051bWJlcihhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdudW1iZXInO1xufVxuXG5mdW5jdGlvbiBpc09iamVjdChhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdvYmplY3QnICYmIGFyZyAhPT0gbnVsbDtcbn1cblxuZnVuY3Rpb24gaXNVbmRlZmluZWQoYXJnKSB7XG4gIHJldHVybiBhcmcgPT09IHZvaWQgMDtcbn1cbiIsIi8vIHNoaW0gZm9yIHVzaW5nIHByb2Nlc3MgaW4gYnJvd3NlclxudmFyIHByb2Nlc3MgPSBtb2R1bGUuZXhwb3J0cyA9IHt9O1xuXG4vLyBjYWNoZWQgZnJvbSB3aGF0ZXZlciBnbG9iYWwgaXMgcHJlc2VudCBzbyB0aGF0IHRlc3QgcnVubmVycyB0aGF0IHN0dWIgaXRcbi8vIGRvbid0IGJyZWFrIHRoaW5ncy4gIEJ1dCB3ZSBuZWVkIHRvIHdyYXAgaXQgaW4gYSB0cnkgY2F0Y2ggaW4gY2FzZSBpdCBpc1xuLy8gd3JhcHBlZCBpbiBzdHJpY3QgbW9kZSBjb2RlIHdoaWNoIGRvZXNuJ3QgZGVmaW5lIGFueSBnbG9iYWxzLiAgSXQncyBpbnNpZGUgYVxuLy8gZnVuY3Rpb24gYmVjYXVzZSB0cnkvY2F0Y2hlcyBkZW9wdGltaXplIGluIGNlcnRhaW4gZW5naW5lcy5cblxudmFyIGNhY2hlZFNldFRpbWVvdXQ7XG52YXIgY2FjaGVkQ2xlYXJUaW1lb3V0O1xuXG5mdW5jdGlvbiBkZWZhdWx0U2V0VGltb3V0KCkge1xuICAgIHRocm93IG5ldyBFcnJvcignc2V0VGltZW91dCBoYXMgbm90IGJlZW4gZGVmaW5lZCcpO1xufVxuZnVuY3Rpb24gZGVmYXVsdENsZWFyVGltZW91dCAoKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdjbGVhclRpbWVvdXQgaGFzIG5vdCBiZWVuIGRlZmluZWQnKTtcbn1cbihmdW5jdGlvbiAoKSB7XG4gICAgdHJ5IHtcbiAgICAgICAgaWYgKHR5cGVvZiBzZXRUaW1lb3V0ID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgICAgICBjYWNoZWRTZXRUaW1lb3V0ID0gc2V0VGltZW91dDtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBkZWZhdWx0U2V0VGltb3V0O1xuICAgICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjYWNoZWRTZXRUaW1lb3V0ID0gZGVmYXVsdFNldFRpbW91dDtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgaWYgKHR5cGVvZiBjbGVhclRpbWVvdXQgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGNsZWFyVGltZW91dDtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGRlZmF1bHRDbGVhclRpbWVvdXQ7XG4gICAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGRlZmF1bHRDbGVhclRpbWVvdXQ7XG4gICAgfVxufSAoKSlcbmZ1bmN0aW9uIHJ1blRpbWVvdXQoZnVuKSB7XG4gICAgaWYgKGNhY2hlZFNldFRpbWVvdXQgPT09IHNldFRpbWVvdXQpIHtcbiAgICAgICAgLy9ub3JtYWwgZW52aXJvbWVudHMgaW4gc2FuZSBzaXR1YXRpb25zXG4gICAgICAgIHJldHVybiBzZXRUaW1lb3V0KGZ1biwgMCk7XG4gICAgfVxuICAgIC8vIGlmIHNldFRpbWVvdXQgd2Fzbid0IGF2YWlsYWJsZSBidXQgd2FzIGxhdHRlciBkZWZpbmVkXG4gICAgaWYgKChjYWNoZWRTZXRUaW1lb3V0ID09PSBkZWZhdWx0U2V0VGltb3V0IHx8ICFjYWNoZWRTZXRUaW1lb3V0KSAmJiBzZXRUaW1lb3V0KSB7XG4gICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBzZXRUaW1lb3V0O1xuICAgICAgICByZXR1cm4gc2V0VGltZW91dChmdW4sIDApO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICAvLyB3aGVuIHdoZW4gc29tZWJvZHkgaGFzIHNjcmV3ZWQgd2l0aCBzZXRUaW1lb3V0IGJ1dCBubyBJLkUuIG1hZGRuZXNzXG4gICAgICAgIHJldHVybiBjYWNoZWRTZXRUaW1lb3V0KGZ1biwgMCk7XG4gICAgfSBjYXRjaChlKXtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIC8vIFdoZW4gd2UgYXJlIGluIEkuRS4gYnV0IHRoZSBzY3JpcHQgaGFzIGJlZW4gZXZhbGVkIHNvIEkuRS4gZG9lc24ndCB0cnVzdCB0aGUgZ2xvYmFsIG9iamVjdCB3aGVuIGNhbGxlZCBub3JtYWxseVxuICAgICAgICAgICAgcmV0dXJuIGNhY2hlZFNldFRpbWVvdXQuY2FsbChudWxsLCBmdW4sIDApO1xuICAgICAgICB9IGNhdGNoKGUpe1xuICAgICAgICAgICAgLy8gc2FtZSBhcyBhYm92ZSBidXQgd2hlbiBpdCdzIGEgdmVyc2lvbiBvZiBJLkUuIHRoYXQgbXVzdCBoYXZlIHRoZSBnbG9iYWwgb2JqZWN0IGZvciAndGhpcycsIGhvcGZ1bGx5IG91ciBjb250ZXh0IGNvcnJlY3Qgb3RoZXJ3aXNlIGl0IHdpbGwgdGhyb3cgYSBnbG9iYWwgZXJyb3JcbiAgICAgICAgICAgIHJldHVybiBjYWNoZWRTZXRUaW1lb3V0LmNhbGwodGhpcywgZnVuLCAwKTtcbiAgICAgICAgfVxuICAgIH1cblxuXG59XG5mdW5jdGlvbiBydW5DbGVhclRpbWVvdXQobWFya2VyKSB7XG4gICAgaWYgKGNhY2hlZENsZWFyVGltZW91dCA9PT0gY2xlYXJUaW1lb3V0KSB7XG4gICAgICAgIC8vbm9ybWFsIGVudmlyb21lbnRzIGluIHNhbmUgc2l0dWF0aW9uc1xuICAgICAgICByZXR1cm4gY2xlYXJUaW1lb3V0KG1hcmtlcik7XG4gICAgfVxuICAgIC8vIGlmIGNsZWFyVGltZW91dCB3YXNuJ3QgYXZhaWxhYmxlIGJ1dCB3YXMgbGF0dGVyIGRlZmluZWRcbiAgICBpZiAoKGNhY2hlZENsZWFyVGltZW91dCA9PT0gZGVmYXVsdENsZWFyVGltZW91dCB8fCAhY2FjaGVkQ2xlYXJUaW1lb3V0KSAmJiBjbGVhclRpbWVvdXQpIHtcbiAgICAgICAgY2FjaGVkQ2xlYXJUaW1lb3V0ID0gY2xlYXJUaW1lb3V0O1xuICAgICAgICByZXR1cm4gY2xlYXJUaW1lb3V0KG1hcmtlcik7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIC8vIHdoZW4gd2hlbiBzb21lYm9keSBoYXMgc2NyZXdlZCB3aXRoIHNldFRpbWVvdXQgYnV0IG5vIEkuRS4gbWFkZG5lc3NcbiAgICAgICAgcmV0dXJuIGNhY2hlZENsZWFyVGltZW91dChtYXJrZXIpO1xuICAgIH0gY2F0Y2ggKGUpe1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgLy8gV2hlbiB3ZSBhcmUgaW4gSS5FLiBidXQgdGhlIHNjcmlwdCBoYXMgYmVlbiBldmFsZWQgc28gSS5FLiBkb2Vzbid0ICB0cnVzdCB0aGUgZ2xvYmFsIG9iamVjdCB3aGVuIGNhbGxlZCBub3JtYWxseVxuICAgICAgICAgICAgcmV0dXJuIGNhY2hlZENsZWFyVGltZW91dC5jYWxsKG51bGwsIG1hcmtlcik7XG4gICAgICAgIH0gY2F0Y2ggKGUpe1xuICAgICAgICAgICAgLy8gc2FtZSBhcyBhYm92ZSBidXQgd2hlbiBpdCdzIGEgdmVyc2lvbiBvZiBJLkUuIHRoYXQgbXVzdCBoYXZlIHRoZSBnbG9iYWwgb2JqZWN0IGZvciAndGhpcycsIGhvcGZ1bGx5IG91ciBjb250ZXh0IGNvcnJlY3Qgb3RoZXJ3aXNlIGl0IHdpbGwgdGhyb3cgYSBnbG9iYWwgZXJyb3IuXG4gICAgICAgICAgICAvLyBTb21lIHZlcnNpb25zIG9mIEkuRS4gaGF2ZSBkaWZmZXJlbnQgcnVsZXMgZm9yIGNsZWFyVGltZW91dCB2cyBzZXRUaW1lb3V0XG4gICAgICAgICAgICByZXR1cm4gY2FjaGVkQ2xlYXJUaW1lb3V0LmNhbGwodGhpcywgbWFya2VyKTtcbiAgICAgICAgfVxuICAgIH1cblxuXG5cbn1cbnZhciBxdWV1ZSA9IFtdO1xudmFyIGRyYWluaW5nID0gZmFsc2U7XG52YXIgY3VycmVudFF1ZXVlO1xudmFyIHF1ZXVlSW5kZXggPSAtMTtcblxuZnVuY3Rpb24gY2xlYW5VcE5leHRUaWNrKCkge1xuICAgIGlmICghZHJhaW5pbmcgfHwgIWN1cnJlbnRRdWV1ZSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGRyYWluaW5nID0gZmFsc2U7XG4gICAgaWYgKGN1cnJlbnRRdWV1ZS5sZW5ndGgpIHtcbiAgICAgICAgcXVldWUgPSBjdXJyZW50UXVldWUuY29uY2F0KHF1ZXVlKTtcbiAgICB9IGVsc2Uge1xuICAgICAgICBxdWV1ZUluZGV4ID0gLTE7XG4gICAgfVxuICAgIGlmIChxdWV1ZS5sZW5ndGgpIHtcbiAgICAgICAgZHJhaW5RdWV1ZSgpO1xuICAgIH1cbn1cblxuZnVuY3Rpb24gZHJhaW5RdWV1ZSgpIHtcbiAgICBpZiAoZHJhaW5pbmcpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB2YXIgdGltZW91dCA9IHJ1blRpbWVvdXQoY2xlYW5VcE5leHRUaWNrKTtcbiAgICBkcmFpbmluZyA9IHRydWU7XG5cbiAgICB2YXIgbGVuID0gcXVldWUubGVuZ3RoO1xuICAgIHdoaWxlKGxlbikge1xuICAgICAgICBjdXJyZW50UXVldWUgPSBxdWV1ZTtcbiAgICAgICAgcXVldWUgPSBbXTtcbiAgICAgICAgd2hpbGUgKCsrcXVldWVJbmRleCA8IGxlbikge1xuICAgICAgICAgICAgaWYgKGN1cnJlbnRRdWV1ZSkge1xuICAgICAgICAgICAgICAgIGN1cnJlbnRRdWV1ZVtxdWV1ZUluZGV4XS5ydW4oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICBxdWV1ZUluZGV4ID0gLTE7XG4gICAgICAgIGxlbiA9IHF1ZXVlLmxlbmd0aDtcbiAgICB9XG4gICAgY3VycmVudFF1ZXVlID0gbnVsbDtcbiAgICBkcmFpbmluZyA9IGZhbHNlO1xuICAgIHJ1bkNsZWFyVGltZW91dCh0aW1lb3V0KTtcbn1cblxucHJvY2Vzcy5uZXh0VGljayA9IGZ1bmN0aW9uIChmdW4pIHtcbiAgICB2YXIgYXJncyA9IG5ldyBBcnJheShhcmd1bWVudHMubGVuZ3RoIC0gMSk7XG4gICAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG4gICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDwgYXJndW1lbnRzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICBhcmdzW2kgLSAxXSA9IGFyZ3VtZW50c1tpXTtcbiAgICAgICAgfVxuICAgIH1cbiAgICBxdWV1ZS5wdXNoKG5ldyBJdGVtKGZ1biwgYXJncykpO1xuICAgIGlmIChxdWV1ZS5sZW5ndGggPT09IDEgJiYgIWRyYWluaW5nKSB7XG4gICAgICAgIHJ1blRpbWVvdXQoZHJhaW5RdWV1ZSk7XG4gICAgfVxufTtcblxuLy8gdjggbGlrZXMgcHJlZGljdGlibGUgb2JqZWN0c1xuZnVuY3Rpb24gSXRlbShmdW4sIGFycmF5KSB7XG4gICAgdGhpcy5mdW4gPSBmdW47XG4gICAgdGhpcy5hcnJheSA9IGFycmF5O1xufVxuSXRlbS5wcm90b3R5cGUucnVuID0gZnVuY3Rpb24gKCkge1xuICAgIHRoaXMuZnVuLmFwcGx5KG51bGwsIHRoaXMuYXJyYXkpO1xufTtcbnByb2Nlc3MudGl0bGUgPSAnYnJvd3Nlcic7XG5wcm9jZXNzLmJyb3dzZXIgPSB0cnVlO1xucHJvY2Vzcy5lbnYgPSB7fTtcbnByb2Nlc3MuYXJndiA9IFtdO1xucHJvY2Vzcy52ZXJzaW9uID0gJyc7IC8vIGVtcHR5IHN0cmluZyB0byBhdm9pZCByZWdleHAgaXNzdWVzXG5wcm9jZXNzLnZlcnNpb25zID0ge307XG5cbmZ1bmN0aW9uIG5vb3AoKSB7fVxuXG5wcm9jZXNzLm9uID0gbm9vcDtcbnByb2Nlc3MuYWRkTGlzdGVuZXIgPSBub29wO1xucHJvY2Vzcy5vbmNlID0gbm9vcDtcbnByb2Nlc3Mub2ZmID0gbm9vcDtcbnByb2Nlc3MucmVtb3ZlTGlzdGVuZXIgPSBub29wO1xucHJvY2Vzcy5yZW1vdmVBbGxMaXN0ZW5lcnMgPSBub29wO1xucHJvY2Vzcy5lbWl0ID0gbm9vcDtcblxucHJvY2Vzcy5iaW5kaW5nID0gZnVuY3Rpb24gKG5hbWUpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ3Byb2Nlc3MuYmluZGluZyBpcyBub3Qgc3VwcG9ydGVkJyk7XG59O1xuXG5wcm9jZXNzLmN3ZCA9IGZ1bmN0aW9uICgpIHsgcmV0dXJuICcvJyB9O1xucHJvY2Vzcy5jaGRpciA9IGZ1bmN0aW9uIChkaXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ3Byb2Nlc3MuY2hkaXIgaXMgbm90IHN1cHBvcnRlZCcpO1xufTtcbnByb2Nlc3MudW1hc2sgPSBmdW5jdGlvbigpIHsgcmV0dXJuIDA7IH07XG4iLCJpZiAodHlwZW9mIE9iamVjdC5jcmVhdGUgPT09ICdmdW5jdGlvbicpIHtcbiAgLy8gaW1wbGVtZW50YXRpb24gZnJvbSBzdGFuZGFyZCBub2RlLmpzICd1dGlsJyBtb2R1bGVcbiAgbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpbmhlcml0cyhjdG9yLCBzdXBlckN0b3IpIHtcbiAgICBjdG9yLnN1cGVyXyA9IHN1cGVyQ3RvclxuICAgIGN0b3IucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckN0b3IucHJvdG90eXBlLCB7XG4gICAgICBjb25zdHJ1Y3Rvcjoge1xuICAgICAgICB2YWx1ZTogY3RvcixcbiAgICAgICAgZW51bWVyYWJsZTogZmFsc2UsXG4gICAgICAgIHdyaXRhYmxlOiB0cnVlLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICAgIH1cbiAgICB9KTtcbiAgfTtcbn0gZWxzZSB7XG4gIC8vIG9sZCBzY2hvb2wgc2hpbSBmb3Igb2xkIGJyb3dzZXJzXG4gIG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaW5oZXJpdHMoY3Rvciwgc3VwZXJDdG9yKSB7XG4gICAgY3Rvci5zdXBlcl8gPSBzdXBlckN0b3JcbiAgICB2YXIgVGVtcEN0b3IgPSBmdW5jdGlvbiAoKSB7fVxuICAgIFRlbXBDdG9yLnByb3RvdHlwZSA9IHN1cGVyQ3Rvci5wcm90b3R5cGVcbiAgICBjdG9yLnByb3RvdHlwZSA9IG5ldyBUZW1wQ3RvcigpXG4gICAgY3Rvci5wcm90b3R5cGUuY29uc3RydWN0b3IgPSBjdG9yXG4gIH1cbn1cbiIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNCdWZmZXIoYXJnKSB7XG4gIHJldHVybiBhcmcgJiYgdHlwZW9mIGFyZyA9PT0gJ29iamVjdCdcbiAgICAmJiB0eXBlb2YgYXJnLmNvcHkgPT09ICdmdW5jdGlvbidcbiAgICAmJiB0eXBlb2YgYXJnLmZpbGwgPT09ICdmdW5jdGlvbidcbiAgICAmJiB0eXBlb2YgYXJnLnJlYWRVSW50OCA9PT0gJ2Z1bmN0aW9uJztcbn0iLCIvLyBDb3B5cmlnaHQgSm95ZW50LCBJbmMuIGFuZCBvdGhlciBOb2RlIGNvbnRyaWJ1dG9ycy5cbi8vXG4vLyBQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYVxuLy8gY29weSBvZiB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZVxuLy8gXCJTb2Z0d2FyZVwiKSwgdG8gZGVhbCBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nXG4vLyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cyB0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsXG4vLyBkaXN0cmlidXRlLCBzdWJsaWNlbnNlLCBhbmQvb3Igc2VsbCBjb3BpZXMgb2YgdGhlIFNvZnR3YXJlLCBhbmQgdG8gcGVybWl0XG4vLyBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGVcbi8vIGZvbGxvd2luZyBjb25kaXRpb25zOlxuLy9cbi8vIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkXG4vLyBpbiBhbGwgY29waWVzIG9yIHN1YnN0YW50aWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS5cbi8vXG4vLyBUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgXCJBUyBJU1wiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTXG4vLyBPUiBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GXG4vLyBNRVJDSEFOVEFCSUxJVFksIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFIEFORCBOT05JTkZSSU5HRU1FTlQuIElOXG4vLyBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SUyBPUiBDT1BZUklHSFQgSE9MREVSUyBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSxcbi8vIERBTUFHRVMgT1IgT1RIRVIgTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUlxuLy8gT1RIRVJXSVNFLCBBUklTSU5HIEZST00sIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRVxuLy8gVVNFIE9SIE9USEVSIERFQUxJTkdTIElOIFRIRSBTT0ZUV0FSRS5cblxudmFyIGZvcm1hdFJlZ0V4cCA9IC8lW3NkaiVdL2c7XG5leHBvcnRzLmZvcm1hdCA9IGZ1bmN0aW9uKGYpIHtcbiAgaWYgKCFpc1N0cmluZyhmKSkge1xuICAgIHZhciBvYmplY3RzID0gW107XG4gICAgZm9yICh2YXIgaSA9IDA7IGkgPCBhcmd1bWVudHMubGVuZ3RoOyBpKyspIHtcbiAgICAgIG9iamVjdHMucHVzaChpbnNwZWN0KGFyZ3VtZW50c1tpXSkpO1xuICAgIH1cbiAgICByZXR1cm4gb2JqZWN0cy5qb2luKCcgJyk7XG4gIH1cblxuICB2YXIgaSA9IDE7XG4gIHZhciBhcmdzID0gYXJndW1lbnRzO1xuICB2YXIgbGVuID0gYXJncy5sZW5ndGg7XG4gIHZhciBzdHIgPSBTdHJpbmcoZikucmVwbGFjZShmb3JtYXRSZWdFeHAsIGZ1bmN0aW9uKHgpIHtcbiAgICBpZiAoeCA9PT0gJyUlJykgcmV0dXJuICclJztcbiAgICBpZiAoaSA+PSBsZW4pIHJldHVybiB4O1xuICAgIHN3aXRjaCAoeCkge1xuICAgICAgY2FzZSAnJXMnOiByZXR1cm4gU3RyaW5nKGFyZ3NbaSsrXSk7XG4gICAgICBjYXNlICclZCc6IHJldHVybiBOdW1iZXIoYXJnc1tpKytdKTtcbiAgICAgIGNhc2UgJyVqJzpcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICByZXR1cm4gSlNPTi5zdHJpbmdpZnkoYXJnc1tpKytdKTtcbiAgICAgICAgfSBjYXRjaCAoXykge1xuICAgICAgICAgIHJldHVybiAnW0NpcmN1bGFyXSc7XG4gICAgICAgIH1cbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHJldHVybiB4O1xuICAgIH1cbiAgfSk7XG4gIGZvciAodmFyIHggPSBhcmdzW2ldOyBpIDwgbGVuOyB4ID0gYXJnc1srK2ldKSB7XG4gICAgaWYgKGlzTnVsbCh4KSB8fCAhaXNPYmplY3QoeCkpIHtcbiAgICAgIHN0ciArPSAnICcgKyB4O1xuICAgIH0gZWxzZSB7XG4gICAgICBzdHIgKz0gJyAnICsgaW5zcGVjdCh4KTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHN0cjtcbn07XG5cblxuLy8gTWFyayB0aGF0IGEgbWV0aG9kIHNob3VsZCBub3QgYmUgdXNlZC5cbi8vIFJldHVybnMgYSBtb2RpZmllZCBmdW5jdGlvbiB3aGljaCB3YXJucyBvbmNlIGJ5IGRlZmF1bHQuXG4vLyBJZiAtLW5vLWRlcHJlY2F0aW9uIGlzIHNldCwgdGhlbiBpdCBpcyBhIG5vLW9wLlxuZXhwb3J0cy5kZXByZWNhdGUgPSBmdW5jdGlvbihmbiwgbXNnKSB7XG4gIC8vIEFsbG93IGZvciBkZXByZWNhdGluZyB0aGluZ3MgaW4gdGhlIHByb2Nlc3Mgb2Ygc3RhcnRpbmcgdXAuXG4gIGlmIChpc1VuZGVmaW5lZChnbG9iYWwucHJvY2VzcykpIHtcbiAgICByZXR1cm4gZnVuY3Rpb24oKSB7XG4gICAgICByZXR1cm4gZXhwb3J0cy5kZXByZWNhdGUoZm4sIG1zZykuYXBwbHkodGhpcywgYXJndW1lbnRzKTtcbiAgICB9O1xuICB9XG5cbiAgaWYgKHByb2Nlc3Mubm9EZXByZWNhdGlvbiA9PT0gdHJ1ZSkge1xuICAgIHJldHVybiBmbjtcbiAgfVxuXG4gIHZhciB3YXJuZWQgPSBmYWxzZTtcbiAgZnVuY3Rpb24gZGVwcmVjYXRlZCgpIHtcbiAgICBpZiAoIXdhcm5lZCkge1xuICAgICAgaWYgKHByb2Nlc3MudGhyb3dEZXByZWNhdGlvbikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IobXNnKTtcbiAgICAgIH0gZWxzZSBpZiAocHJvY2Vzcy50cmFjZURlcHJlY2F0aW9uKSB7XG4gICAgICAgIGNvbnNvbGUudHJhY2UobXNnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IobXNnKTtcbiAgICAgIH1cbiAgICAgIHdhcm5lZCA9IHRydWU7XG4gICAgfVxuICAgIHJldHVybiBmbi5hcHBseSh0aGlzLCBhcmd1bWVudHMpO1xuICB9XG5cbiAgcmV0dXJuIGRlcHJlY2F0ZWQ7XG59O1xuXG5cbnZhciBkZWJ1Z3MgPSB7fTtcbnZhciBkZWJ1Z0Vudmlyb247XG5leHBvcnRzLmRlYnVnbG9nID0gZnVuY3Rpb24oc2V0KSB7XG4gIGlmIChpc1VuZGVmaW5lZChkZWJ1Z0Vudmlyb24pKVxuICAgIGRlYnVnRW52aXJvbiA9IHByb2Nlc3MuZW52Lk5PREVfREVCVUcgfHwgJyc7XG4gIHNldCA9IHNldC50b1VwcGVyQ2FzZSgpO1xuICBpZiAoIWRlYnVnc1tzZXRdKSB7XG4gICAgaWYgKG5ldyBSZWdFeHAoJ1xcXFxiJyArIHNldCArICdcXFxcYicsICdpJykudGVzdChkZWJ1Z0Vudmlyb24pKSB7XG4gICAgICB2YXIgcGlkID0gcHJvY2Vzcy5waWQ7XG4gICAgICBkZWJ1Z3Nbc2V0XSA9IGZ1bmN0aW9uKCkge1xuICAgICAgICB2YXIgbXNnID0gZXhwb3J0cy5mb3JtYXQuYXBwbHkoZXhwb3J0cywgYXJndW1lbnRzKTtcbiAgICAgICAgY29uc29sZS5lcnJvcignJXMgJWQ6ICVzJywgc2V0LCBwaWQsIG1zZyk7XG4gICAgICB9O1xuICAgIH0gZWxzZSB7XG4gICAgICBkZWJ1Z3Nbc2V0XSA9IGZ1bmN0aW9uKCkge307XG4gICAgfVxuICB9XG4gIHJldHVybiBkZWJ1Z3Nbc2V0XTtcbn07XG5cblxuLyoqXG4gKiBFY2hvcyB0aGUgdmFsdWUgb2YgYSB2YWx1ZS4gVHJ5cyB0byBwcmludCB0aGUgdmFsdWUgb3V0XG4gKiBpbiB0aGUgYmVzdCB3YXkgcG9zc2libGUgZ2l2ZW4gdGhlIGRpZmZlcmVudCB0eXBlcy5cbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gb2JqIFRoZSBvYmplY3QgdG8gcHJpbnQgb3V0LlxuICogQHBhcmFtIHtPYmplY3R9IG9wdHMgT3B0aW9uYWwgb3B0aW9ucyBvYmplY3QgdGhhdCBhbHRlcnMgdGhlIG91dHB1dC5cbiAqL1xuLyogbGVnYWN5OiBvYmosIHNob3dIaWRkZW4sIGRlcHRoLCBjb2xvcnMqL1xuZnVuY3Rpb24gaW5zcGVjdChvYmosIG9wdHMpIHtcbiAgLy8gZGVmYXVsdCBvcHRpb25zXG4gIHZhciBjdHggPSB7XG4gICAgc2VlbjogW10sXG4gICAgc3R5bGl6ZTogc3R5bGl6ZU5vQ29sb3JcbiAgfTtcbiAgLy8gbGVnYWN5Li4uXG4gIGlmIChhcmd1bWVudHMubGVuZ3RoID49IDMpIGN0eC5kZXB0aCA9IGFyZ3VtZW50c1syXTtcbiAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPj0gNCkgY3R4LmNvbG9ycyA9IGFyZ3VtZW50c1szXTtcbiAgaWYgKGlzQm9vbGVhbihvcHRzKSkge1xuICAgIC8vIGxlZ2FjeS4uLlxuICAgIGN0eC5zaG93SGlkZGVuID0gb3B0cztcbiAgfSBlbHNlIGlmIChvcHRzKSB7XG4gICAgLy8gZ290IGFuIFwib3B0aW9uc1wiIG9iamVjdFxuICAgIGV4cG9ydHMuX2V4dGVuZChjdHgsIG9wdHMpO1xuICB9XG4gIC8vIHNldCBkZWZhdWx0IG9wdGlvbnNcbiAgaWYgKGlzVW5kZWZpbmVkKGN0eC5zaG93SGlkZGVuKSkgY3R4LnNob3dIaWRkZW4gPSBmYWxzZTtcbiAgaWYgKGlzVW5kZWZpbmVkKGN0eC5kZXB0aCkpIGN0eC5kZXB0aCA9IDI7XG4gIGlmIChpc1VuZGVmaW5lZChjdHguY29sb3JzKSkgY3R4LmNvbG9ycyA9IGZhbHNlO1xuICBpZiAoaXNVbmRlZmluZWQoY3R4LmN1c3RvbUluc3BlY3QpKSBjdHguY3VzdG9tSW5zcGVjdCA9IHRydWU7XG4gIGlmIChjdHguY29sb3JzKSBjdHguc3R5bGl6ZSA9IHN0eWxpemVXaXRoQ29sb3I7XG4gIHJldHVybiBmb3JtYXRWYWx1ZShjdHgsIG9iaiwgY3R4LmRlcHRoKTtcbn1cbmV4cG9ydHMuaW5zcGVjdCA9IGluc3BlY3Q7XG5cblxuLy8gaHR0cDovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9BTlNJX2VzY2FwZV9jb2RlI2dyYXBoaWNzXG5pbnNwZWN0LmNvbG9ycyA9IHtcbiAgJ2JvbGQnIDogWzEsIDIyXSxcbiAgJ2l0YWxpYycgOiBbMywgMjNdLFxuICAndW5kZXJsaW5lJyA6IFs0LCAyNF0sXG4gICdpbnZlcnNlJyA6IFs3LCAyN10sXG4gICd3aGl0ZScgOiBbMzcsIDM5XSxcbiAgJ2dyZXknIDogWzkwLCAzOV0sXG4gICdibGFjaycgOiBbMzAsIDM5XSxcbiAgJ2JsdWUnIDogWzM0LCAzOV0sXG4gICdjeWFuJyA6IFszNiwgMzldLFxuICAnZ3JlZW4nIDogWzMyLCAzOV0sXG4gICdtYWdlbnRhJyA6IFszNSwgMzldLFxuICAncmVkJyA6IFszMSwgMzldLFxuICAneWVsbG93JyA6IFszMywgMzldXG59O1xuXG4vLyBEb24ndCB1c2UgJ2JsdWUnIG5vdCB2aXNpYmxlIG9uIGNtZC5leGVcbmluc3BlY3Quc3R5bGVzID0ge1xuICAnc3BlY2lhbCc6ICdjeWFuJyxcbiAgJ251bWJlcic6ICd5ZWxsb3cnLFxuICAnYm9vbGVhbic6ICd5ZWxsb3cnLFxuICAndW5kZWZpbmVkJzogJ2dyZXknLFxuICAnbnVsbCc6ICdib2xkJyxcbiAgJ3N0cmluZyc6ICdncmVlbicsXG4gICdkYXRlJzogJ21hZ2VudGEnLFxuICAvLyBcIm5hbWVcIjogaW50ZW50aW9uYWxseSBub3Qgc3R5bGluZ1xuICAncmVnZXhwJzogJ3JlZCdcbn07XG5cblxuZnVuY3Rpb24gc3R5bGl6ZVdpdGhDb2xvcihzdHIsIHN0eWxlVHlwZSkge1xuICB2YXIgc3R5bGUgPSBpbnNwZWN0LnN0eWxlc1tzdHlsZVR5cGVdO1xuXG4gIGlmIChzdHlsZSkge1xuICAgIHJldHVybiAnXFx1MDAxYlsnICsgaW5zcGVjdC5jb2xvcnNbc3R5bGVdWzBdICsgJ20nICsgc3RyICtcbiAgICAgICAgICAgJ1xcdTAwMWJbJyArIGluc3BlY3QuY29sb3JzW3N0eWxlXVsxXSArICdtJztcbiAgfSBlbHNlIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG59XG5cblxuZnVuY3Rpb24gc3R5bGl6ZU5vQ29sb3Ioc3RyLCBzdHlsZVR5cGUpIHtcbiAgcmV0dXJuIHN0cjtcbn1cblxuXG5mdW5jdGlvbiBhcnJheVRvSGFzaChhcnJheSkge1xuICB2YXIgaGFzaCA9IHt9O1xuXG4gIGFycmF5LmZvckVhY2goZnVuY3Rpb24odmFsLCBpZHgpIHtcbiAgICBoYXNoW3ZhbF0gPSB0cnVlO1xuICB9KTtcblxuICByZXR1cm4gaGFzaDtcbn1cblxuXG5mdW5jdGlvbiBmb3JtYXRWYWx1ZShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMpIHtcbiAgLy8gUHJvdmlkZSBhIGhvb2sgZm9yIHVzZXItc3BlY2lmaWVkIGluc3BlY3QgZnVuY3Rpb25zLlxuICAvLyBDaGVjayB0aGF0IHZhbHVlIGlzIGFuIG9iamVjdCB3aXRoIGFuIGluc3BlY3QgZnVuY3Rpb24gb24gaXRcbiAgaWYgKGN0eC5jdXN0b21JbnNwZWN0ICYmXG4gICAgICB2YWx1ZSAmJlxuICAgICAgaXNGdW5jdGlvbih2YWx1ZS5pbnNwZWN0KSAmJlxuICAgICAgLy8gRmlsdGVyIG91dCB0aGUgdXRpbCBtb2R1bGUsIGl0J3MgaW5zcGVjdCBmdW5jdGlvbiBpcyBzcGVjaWFsXG4gICAgICB2YWx1ZS5pbnNwZWN0ICE9PSBleHBvcnRzLmluc3BlY3QgJiZcbiAgICAgIC8vIEFsc28gZmlsdGVyIG91dCBhbnkgcHJvdG90eXBlIG9iamVjdHMgdXNpbmcgdGhlIGNpcmN1bGFyIGNoZWNrLlxuICAgICAgISh2YWx1ZS5jb25zdHJ1Y3RvciAmJiB2YWx1ZS5jb25zdHJ1Y3Rvci5wcm90b3R5cGUgPT09IHZhbHVlKSkge1xuICAgIHZhciByZXQgPSB2YWx1ZS5pbnNwZWN0KHJlY3Vyc2VUaW1lcywgY3R4KTtcbiAgICBpZiAoIWlzU3RyaW5nKHJldCkpIHtcbiAgICAgIHJldCA9IGZvcm1hdFZhbHVlKGN0eCwgcmV0LCByZWN1cnNlVGltZXMpO1xuICAgIH1cbiAgICByZXR1cm4gcmV0O1xuICB9XG5cbiAgLy8gUHJpbWl0aXZlIHR5cGVzIGNhbm5vdCBoYXZlIHByb3BlcnRpZXNcbiAgdmFyIHByaW1pdGl2ZSA9IGZvcm1hdFByaW1pdGl2ZShjdHgsIHZhbHVlKTtcbiAgaWYgKHByaW1pdGl2ZSkge1xuICAgIHJldHVybiBwcmltaXRpdmU7XG4gIH1cblxuICAvLyBMb29rIHVwIHRoZSBrZXlzIG9mIHRoZSBvYmplY3QuXG4gIHZhciBrZXlzID0gT2JqZWN0LmtleXModmFsdWUpO1xuICB2YXIgdmlzaWJsZUtleXMgPSBhcnJheVRvSGFzaChrZXlzKTtcblxuICBpZiAoY3R4LnNob3dIaWRkZW4pIHtcbiAgICBrZXlzID0gT2JqZWN0LmdldE93blByb3BlcnR5TmFtZXModmFsdWUpO1xuICB9XG5cbiAgLy8gSUUgZG9lc24ndCBtYWtlIGVycm9yIGZpZWxkcyBub24tZW51bWVyYWJsZVxuICAvLyBodHRwOi8vbXNkbi5taWNyb3NvZnQuY29tL2VuLXVzL2xpYnJhcnkvaWUvZHd3NTJzYnQodj12cy45NCkuYXNweFxuICBpZiAoaXNFcnJvcih2YWx1ZSlcbiAgICAgICYmIChrZXlzLmluZGV4T2YoJ21lc3NhZ2UnKSA+PSAwIHx8IGtleXMuaW5kZXhPZignZGVzY3JpcHRpb24nKSA+PSAwKSkge1xuICAgIHJldHVybiBmb3JtYXRFcnJvcih2YWx1ZSk7XG4gIH1cblxuICAvLyBTb21lIHR5cGUgb2Ygb2JqZWN0IHdpdGhvdXQgcHJvcGVydGllcyBjYW4gYmUgc2hvcnRjdXR0ZWQuXG4gIGlmIChrZXlzLmxlbmd0aCA9PT0gMCkge1xuICAgIGlmIChpc0Z1bmN0aW9uKHZhbHVlKSkge1xuICAgICAgdmFyIG5hbWUgPSB2YWx1ZS5uYW1lID8gJzogJyArIHZhbHVlLm5hbWUgOiAnJztcbiAgICAgIHJldHVybiBjdHguc3R5bGl6ZSgnW0Z1bmN0aW9uJyArIG5hbWUgKyAnXScsICdzcGVjaWFsJyk7XG4gICAgfVxuICAgIGlmIChpc1JlZ0V4cCh2YWx1ZSkpIHtcbiAgICAgIHJldHVybiBjdHguc3R5bGl6ZShSZWdFeHAucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwodmFsdWUpLCAncmVnZXhwJyk7XG4gICAgfVxuICAgIGlmIChpc0RhdGUodmFsdWUpKSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoRGF0ZS5wcm90b3R5cGUudG9TdHJpbmcuY2FsbCh2YWx1ZSksICdkYXRlJyk7XG4gICAgfVxuICAgIGlmIChpc0Vycm9yKHZhbHVlKSkge1xuICAgICAgcmV0dXJuIGZvcm1hdEVycm9yKHZhbHVlKTtcbiAgICB9XG4gIH1cblxuICB2YXIgYmFzZSA9ICcnLCBhcnJheSA9IGZhbHNlLCBicmFjZXMgPSBbJ3snLCAnfSddO1xuXG4gIC8vIE1ha2UgQXJyYXkgc2F5IHRoYXQgdGhleSBhcmUgQXJyYXlcbiAgaWYgKGlzQXJyYXkodmFsdWUpKSB7XG4gICAgYXJyYXkgPSB0cnVlO1xuICAgIGJyYWNlcyA9IFsnWycsICddJ107XG4gIH1cblxuICAvLyBNYWtlIGZ1bmN0aW9ucyBzYXkgdGhhdCB0aGV5IGFyZSBmdW5jdGlvbnNcbiAgaWYgKGlzRnVuY3Rpb24odmFsdWUpKSB7XG4gICAgdmFyIG4gPSB2YWx1ZS5uYW1lID8gJzogJyArIHZhbHVlLm5hbWUgOiAnJztcbiAgICBiYXNlID0gJyBbRnVuY3Rpb24nICsgbiArICddJztcbiAgfVxuXG4gIC8vIE1ha2UgUmVnRXhwcyBzYXkgdGhhdCB0aGV5IGFyZSBSZWdFeHBzXG4gIGlmIChpc1JlZ0V4cCh2YWx1ZSkpIHtcbiAgICBiYXNlID0gJyAnICsgUmVnRXhwLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKTtcbiAgfVxuXG4gIC8vIE1ha2UgZGF0ZXMgd2l0aCBwcm9wZXJ0aWVzIGZpcnN0IHNheSB0aGUgZGF0ZVxuICBpZiAoaXNEYXRlKHZhbHVlKSkge1xuICAgIGJhc2UgPSAnICcgKyBEYXRlLnByb3RvdHlwZS50b1VUQ1N0cmluZy5jYWxsKHZhbHVlKTtcbiAgfVxuXG4gIC8vIE1ha2UgZXJyb3Igd2l0aCBtZXNzYWdlIGZpcnN0IHNheSB0aGUgZXJyb3JcbiAgaWYgKGlzRXJyb3IodmFsdWUpKSB7XG4gICAgYmFzZSA9ICcgJyArIGZvcm1hdEVycm9yKHZhbHVlKTtcbiAgfVxuXG4gIGlmIChrZXlzLmxlbmd0aCA9PT0gMCAmJiAoIWFycmF5IHx8IHZhbHVlLmxlbmd0aCA9PSAwKSkge1xuICAgIHJldHVybiBicmFjZXNbMF0gKyBiYXNlICsgYnJhY2VzWzFdO1xuICB9XG5cbiAgaWYgKHJlY3Vyc2VUaW1lcyA8IDApIHtcbiAgICBpZiAoaXNSZWdFeHAodmFsdWUpKSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoUmVnRXhwLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSwgJ3JlZ2V4cCcpO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoJ1tPYmplY3RdJywgJ3NwZWNpYWwnKTtcbiAgICB9XG4gIH1cblxuICBjdHguc2Vlbi5wdXNoKHZhbHVlKTtcblxuICB2YXIgb3V0cHV0O1xuICBpZiAoYXJyYXkpIHtcbiAgICBvdXRwdXQgPSBmb3JtYXRBcnJheShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXlzKTtcbiAgfSBlbHNlIHtcbiAgICBvdXRwdXQgPSBrZXlzLm1hcChmdW5jdGlvbihrZXkpIHtcbiAgICAgIHJldHVybiBmb3JtYXRQcm9wZXJ0eShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXksIGFycmF5KTtcbiAgICB9KTtcbiAgfVxuXG4gIGN0eC5zZWVuLnBvcCgpO1xuXG4gIHJldHVybiByZWR1Y2VUb1NpbmdsZVN0cmluZyhvdXRwdXQsIGJhc2UsIGJyYWNlcyk7XG59XG5cblxuZnVuY3Rpb24gZm9ybWF0UHJpbWl0aXZlKGN0eCwgdmFsdWUpIHtcbiAgaWYgKGlzVW5kZWZpbmVkKHZhbHVlKSlcbiAgICByZXR1cm4gY3R4LnN0eWxpemUoJ3VuZGVmaW5lZCcsICd1bmRlZmluZWQnKTtcbiAgaWYgKGlzU3RyaW5nKHZhbHVlKSkge1xuICAgIHZhciBzaW1wbGUgPSAnXFwnJyArIEpTT04uc3RyaW5naWZ5KHZhbHVlKS5yZXBsYWNlKC9eXCJ8XCIkL2csICcnKVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnJlcGxhY2UoLycvZywgXCJcXFxcJ1wiKVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnJlcGxhY2UoL1xcXFxcIi9nLCAnXCInKSArICdcXCcnO1xuICAgIHJldHVybiBjdHguc3R5bGl6ZShzaW1wbGUsICdzdHJpbmcnKTtcbiAgfVxuICBpZiAoaXNOdW1iZXIodmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnJyArIHZhbHVlLCAnbnVtYmVyJyk7XG4gIGlmIChpc0Jvb2xlYW4odmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnJyArIHZhbHVlLCAnYm9vbGVhbicpO1xuICAvLyBGb3Igc29tZSByZWFzb24gdHlwZW9mIG51bGwgaXMgXCJvYmplY3RcIiwgc28gc3BlY2lhbCBjYXNlIGhlcmUuXG4gIGlmIChpc051bGwodmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnbnVsbCcsICdudWxsJyk7XG59XG5cblxuZnVuY3Rpb24gZm9ybWF0RXJyb3IodmFsdWUpIHtcbiAgcmV0dXJuICdbJyArIEVycm9yLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSArICddJztcbn1cblxuXG5mdW5jdGlvbiBmb3JtYXRBcnJheShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXlzKSB7XG4gIHZhciBvdXRwdXQgPSBbXTtcbiAgZm9yICh2YXIgaSA9IDAsIGwgPSB2YWx1ZS5sZW5ndGg7IGkgPCBsOyArK2kpIHtcbiAgICBpZiAoaGFzT3duUHJvcGVydHkodmFsdWUsIFN0cmluZyhpKSkpIHtcbiAgICAgIG91dHB1dC5wdXNoKGZvcm1hdFByb3BlcnR5KGN0eCwgdmFsdWUsIHJlY3Vyc2VUaW1lcywgdmlzaWJsZUtleXMsXG4gICAgICAgICAgU3RyaW5nKGkpLCB0cnVlKSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIG91dHB1dC5wdXNoKCcnKTtcbiAgICB9XG4gIH1cbiAga2V5cy5mb3JFYWNoKGZ1bmN0aW9uKGtleSkge1xuICAgIGlmICgha2V5Lm1hdGNoKC9eXFxkKyQvKSkge1xuICAgICAgb3V0cHV0LnB1c2goZm9ybWF0UHJvcGVydHkoY3R4LCB2YWx1ZSwgcmVjdXJzZVRpbWVzLCB2aXNpYmxlS2V5cyxcbiAgICAgICAgICBrZXksIHRydWUpKTtcbiAgICB9XG4gIH0pO1xuICByZXR1cm4gb3V0cHV0O1xufVxuXG5cbmZ1bmN0aW9uIGZvcm1hdFByb3BlcnR5KGN0eCwgdmFsdWUsIHJlY3Vyc2VUaW1lcywgdmlzaWJsZUtleXMsIGtleSwgYXJyYXkpIHtcbiAgdmFyIG5hbWUsIHN0ciwgZGVzYztcbiAgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IodmFsdWUsIGtleSkgfHwgeyB2YWx1ZTogdmFsdWVba2V5XSB9O1xuICBpZiAoZGVzYy5nZXQpIHtcbiAgICBpZiAoZGVzYy5zZXQpIHtcbiAgICAgIHN0ciA9IGN0eC5zdHlsaXplKCdbR2V0dGVyL1NldHRlcl0nLCAnc3BlY2lhbCcpO1xuICAgIH0gZWxzZSB7XG4gICAgICBzdHIgPSBjdHguc3R5bGl6ZSgnW0dldHRlcl0nLCAnc3BlY2lhbCcpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBpZiAoZGVzYy5zZXQpIHtcbiAgICAgIHN0ciA9IGN0eC5zdHlsaXplKCdbU2V0dGVyXScsICdzcGVjaWFsJyk7XG4gICAgfVxuICB9XG4gIGlmICghaGFzT3duUHJvcGVydHkodmlzaWJsZUtleXMsIGtleSkpIHtcbiAgICBuYW1lID0gJ1snICsga2V5ICsgJ10nO1xuICB9XG4gIGlmICghc3RyKSB7XG4gICAgaWYgKGN0eC5zZWVuLmluZGV4T2YoZGVzYy52YWx1ZSkgPCAwKSB7XG4gICAgICBpZiAoaXNOdWxsKHJlY3Vyc2VUaW1lcykpIHtcbiAgICAgICAgc3RyID0gZm9ybWF0VmFsdWUoY3R4LCBkZXNjLnZhbHVlLCBudWxsKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHN0ciA9IGZvcm1hdFZhbHVlKGN0eCwgZGVzYy52YWx1ZSwgcmVjdXJzZVRpbWVzIC0gMSk7XG4gICAgICB9XG4gICAgICBpZiAoc3RyLmluZGV4T2YoJ1xcbicpID4gLTEpIHtcbiAgICAgICAgaWYgKGFycmF5KSB7XG4gICAgICAgICAgc3RyID0gc3RyLnNwbGl0KCdcXG4nKS5tYXAoZnVuY3Rpb24obGluZSkge1xuICAgICAgICAgICAgcmV0dXJuICcgICcgKyBsaW5lO1xuICAgICAgICAgIH0pLmpvaW4oJ1xcbicpLnN1YnN0cigyKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBzdHIgPSAnXFxuJyArIHN0ci5zcGxpdCgnXFxuJykubWFwKGZ1bmN0aW9uKGxpbmUpIHtcbiAgICAgICAgICAgIHJldHVybiAnICAgJyArIGxpbmU7XG4gICAgICAgICAgfSkuam9pbignXFxuJyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgc3RyID0gY3R4LnN0eWxpemUoJ1tDaXJjdWxhcl0nLCAnc3BlY2lhbCcpO1xuICAgIH1cbiAgfVxuICBpZiAoaXNVbmRlZmluZWQobmFtZSkpIHtcbiAgICBpZiAoYXJyYXkgJiYga2V5Lm1hdGNoKC9eXFxkKyQvKSkge1xuICAgICAgcmV0dXJuIHN0cjtcbiAgICB9XG4gICAgbmFtZSA9IEpTT04uc3RyaW5naWZ5KCcnICsga2V5KTtcbiAgICBpZiAobmFtZS5tYXRjaCgvXlwiKFthLXpBLVpfXVthLXpBLVpfMC05XSopXCIkLykpIHtcbiAgICAgIG5hbWUgPSBuYW1lLnN1YnN0cigxLCBuYW1lLmxlbmd0aCAtIDIpO1xuICAgICAgbmFtZSA9IGN0eC5zdHlsaXplKG5hbWUsICduYW1lJyk7XG4gICAgfSBlbHNlIHtcbiAgICAgIG5hbWUgPSBuYW1lLnJlcGxhY2UoLycvZywgXCJcXFxcJ1wiKVxuICAgICAgICAgICAgICAgICAucmVwbGFjZSgvXFxcXFwiL2csICdcIicpXG4gICAgICAgICAgICAgICAgIC5yZXBsYWNlKC8oXlwifFwiJCkvZywgXCInXCIpO1xuICAgICAgbmFtZSA9IGN0eC5zdHlsaXplKG5hbWUsICdzdHJpbmcnKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gbmFtZSArICc6ICcgKyBzdHI7XG59XG5cblxuZnVuY3Rpb24gcmVkdWNlVG9TaW5nbGVTdHJpbmcob3V0cHV0LCBiYXNlLCBicmFjZXMpIHtcbiAgdmFyIG51bUxpbmVzRXN0ID0gMDtcbiAgdmFyIGxlbmd0aCA9IG91dHB1dC5yZWR1Y2UoZnVuY3Rpb24ocHJldiwgY3VyKSB7XG4gICAgbnVtTGluZXNFc3QrKztcbiAgICBpZiAoY3VyLmluZGV4T2YoJ1xcbicpID49IDApIG51bUxpbmVzRXN0Kys7XG4gICAgcmV0dXJuIHByZXYgKyBjdXIucmVwbGFjZSgvXFx1MDAxYlxcW1xcZFxcZD9tL2csICcnKS5sZW5ndGggKyAxO1xuICB9LCAwKTtcblxuICBpZiAobGVuZ3RoID4gNjApIHtcbiAgICByZXR1cm4gYnJhY2VzWzBdICtcbiAgICAgICAgICAgKGJhc2UgPT09ICcnID8gJycgOiBiYXNlICsgJ1xcbiAnKSArXG4gICAgICAgICAgICcgJyArXG4gICAgICAgICAgIG91dHB1dC5qb2luKCcsXFxuICAnKSArXG4gICAgICAgICAgICcgJyArXG4gICAgICAgICAgIGJyYWNlc1sxXTtcbiAgfVxuXG4gIHJldHVybiBicmFjZXNbMF0gKyBiYXNlICsgJyAnICsgb3V0cHV0LmpvaW4oJywgJykgKyAnICcgKyBicmFjZXNbMV07XG59XG5cblxuLy8gTk9URTogVGhlc2UgdHlwZSBjaGVja2luZyBmdW5jdGlvbnMgaW50ZW50aW9uYWxseSBkb24ndCB1c2UgYGluc3RhbmNlb2ZgXG4vLyBiZWNhdXNlIGl0IGlzIGZyYWdpbGUgYW5kIGNhbiBiZSBlYXNpbHkgZmFrZWQgd2l0aCBgT2JqZWN0LmNyZWF0ZSgpYC5cbmZ1bmN0aW9uIGlzQXJyYXkoYXIpIHtcbiAgcmV0dXJuIEFycmF5LmlzQXJyYXkoYXIpO1xufVxuZXhwb3J0cy5pc0FycmF5ID0gaXNBcnJheTtcblxuZnVuY3Rpb24gaXNCb29sZWFuKGFyZykge1xuICByZXR1cm4gdHlwZW9mIGFyZyA9PT0gJ2Jvb2xlYW4nO1xufVxuZXhwb3J0cy5pc0Jvb2xlYW4gPSBpc0Jvb2xlYW47XG5cbmZ1bmN0aW9uIGlzTnVsbChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PT0gbnVsbDtcbn1cbmV4cG9ydHMuaXNOdWxsID0gaXNOdWxsO1xuXG5mdW5jdGlvbiBpc051bGxPclVuZGVmaW5lZChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PSBudWxsO1xufVxuZXhwb3J0cy5pc051bGxPclVuZGVmaW5lZCA9IGlzTnVsbE9yVW5kZWZpbmVkO1xuXG5mdW5jdGlvbiBpc051bWJlcihhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdudW1iZXInO1xufVxuZXhwb3J0cy5pc051bWJlciA9IGlzTnVtYmVyO1xuXG5mdW5jdGlvbiBpc1N0cmluZyhhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdzdHJpbmcnO1xufVxuZXhwb3J0cy5pc1N0cmluZyA9IGlzU3RyaW5nO1xuXG5mdW5jdGlvbiBpc1N5bWJvbChhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdzeW1ib2wnO1xufVxuZXhwb3J0cy5pc1N5bWJvbCA9IGlzU3ltYm9sO1xuXG5mdW5jdGlvbiBpc1VuZGVmaW5lZChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PT0gdm9pZCAwO1xufVxuZXhwb3J0cy5pc1VuZGVmaW5lZCA9IGlzVW5kZWZpbmVkO1xuXG5mdW5jdGlvbiBpc1JlZ0V4cChyZSkge1xuICByZXR1cm4gaXNPYmplY3QocmUpICYmIG9iamVjdFRvU3RyaW5nKHJlKSA9PT0gJ1tvYmplY3QgUmVnRXhwXSc7XG59XG5leHBvcnRzLmlzUmVnRXhwID0gaXNSZWdFeHA7XG5cbmZ1bmN0aW9uIGlzT2JqZWN0KGFyZykge1xuICByZXR1cm4gdHlwZW9mIGFyZyA9PT0gJ29iamVjdCcgJiYgYXJnICE9PSBudWxsO1xufVxuZXhwb3J0cy5pc09iamVjdCA9IGlzT2JqZWN0O1xuXG5mdW5jdGlvbiBpc0RhdGUoZCkge1xuICByZXR1cm4gaXNPYmplY3QoZCkgJiYgb2JqZWN0VG9TdHJpbmcoZCkgPT09ICdbb2JqZWN0IERhdGVdJztcbn1cbmV4cG9ydHMuaXNEYXRlID0gaXNEYXRlO1xuXG5mdW5jdGlvbiBpc0Vycm9yKGUpIHtcbiAgcmV0dXJuIGlzT2JqZWN0KGUpICYmXG4gICAgICAob2JqZWN0VG9TdHJpbmcoZSkgPT09ICdbb2JqZWN0IEVycm9yXScgfHwgZSBpbnN0YW5jZW9mIEVycm9yKTtcbn1cbmV4cG9ydHMuaXNFcnJvciA9IGlzRXJyb3I7XG5cbmZ1bmN0aW9uIGlzRnVuY3Rpb24oYXJnKSB7XG4gIHJldHVybiB0eXBlb2YgYXJnID09PSAnZnVuY3Rpb24nO1xufVxuZXhwb3J0cy5pc0Z1bmN0aW9uID0gaXNGdW5jdGlvbjtcblxuZnVuY3Rpb24gaXNQcmltaXRpdmUoYXJnKSB7XG4gIHJldHVybiBhcmcgPT09IG51bGwgfHxcbiAgICAgICAgIHR5cGVvZiBhcmcgPT09ICdib29sZWFuJyB8fFxuICAgICAgICAgdHlwZW9mIGFyZyA9PT0gJ251bWJlcicgfHxcbiAgICAgICAgIHR5cGVvZiBhcmcgPT09ICdzdHJpbmcnIHx8XG4gICAgICAgICB0eXBlb2YgYXJnID09PSAnc3ltYm9sJyB8fCAgLy8gRVM2IHN5bWJvbFxuICAgICAgICAgdHlwZW9mIGFyZyA9PT0gJ3VuZGVmaW5lZCc7XG59XG5leHBvcnRzLmlzUHJpbWl0aXZlID0gaXNQcmltaXRpdmU7XG5cbmV4cG9ydHMuaXNCdWZmZXIgPSByZXF1aXJlKCcuL3N1cHBvcnQvaXNCdWZmZXInKTtcblxuZnVuY3Rpb24gb2JqZWN0VG9TdHJpbmcobykge1xuICByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKG8pO1xufVxuXG5cbmZ1bmN0aW9uIHBhZChuKSB7XG4gIHJldHVybiBuIDwgMTAgPyAnMCcgKyBuLnRvU3RyaW5nKDEwKSA6IG4udG9TdHJpbmcoMTApO1xufVxuXG5cbnZhciBtb250aHMgPSBbJ0phbicsICdGZWInLCAnTWFyJywgJ0FwcicsICdNYXknLCAnSnVuJywgJ0p1bCcsICdBdWcnLCAnU2VwJyxcbiAgICAgICAgICAgICAgJ09jdCcsICdOb3YnLCAnRGVjJ107XG5cbi8vIDI2IEZlYiAxNjoxOTozNFxuZnVuY3Rpb24gdGltZXN0YW1wKCkge1xuICB2YXIgZCA9IG5ldyBEYXRlKCk7XG4gIHZhciB0aW1lID0gW3BhZChkLmdldEhvdXJzKCkpLFxuICAgICAgICAgICAgICBwYWQoZC5nZXRNaW51dGVzKCkpLFxuICAgICAgICAgICAgICBwYWQoZC5nZXRTZWNvbmRzKCkpXS5qb2luKCc6Jyk7XG4gIHJldHVybiBbZC5nZXREYXRlKCksIG1vbnRoc1tkLmdldE1vbnRoKCldLCB0aW1lXS5qb2luKCcgJyk7XG59XG5cblxuLy8gbG9nIGlzIGp1c3QgYSB0aGluIHdyYXBwZXIgdG8gY29uc29sZS5sb2cgdGhhdCBwcmVwZW5kcyBhIHRpbWVzdGFtcFxuZXhwb3J0cy5sb2cgPSBmdW5jdGlvbigpIHtcbiAgY29uc29sZS5sb2coJyVzIC0gJXMnLCB0aW1lc3RhbXAoKSwgZXhwb3J0cy5mb3JtYXQuYXBwbHkoZXhwb3J0cywgYXJndW1lbnRzKSk7XG59O1xuXG5cbi8qKlxuICogSW5oZXJpdCB0aGUgcHJvdG90eXBlIG1ldGhvZHMgZnJvbSBvbmUgY29uc3RydWN0b3IgaW50byBhbm90aGVyLlxuICpcbiAqIFRoZSBGdW5jdGlvbi5wcm90b3R5cGUuaW5oZXJpdHMgZnJvbSBsYW5nLmpzIHJld3JpdHRlbiBhcyBhIHN0YW5kYWxvbmVcbiAqIGZ1bmN0aW9uIChub3Qgb24gRnVuY3Rpb24ucHJvdG90eXBlKS4gTk9URTogSWYgdGhpcyBmaWxlIGlzIHRvIGJlIGxvYWRlZFxuICogZHVyaW5nIGJvb3RzdHJhcHBpbmcgdGhpcyBmdW5jdGlvbiBuZWVkcyB0byBiZSByZXdyaXR0ZW4gdXNpbmcgc29tZSBuYXRpdmVcbiAqIGZ1bmN0aW9ucyBhcyBwcm90b3R5cGUgc2V0dXAgdXNpbmcgbm9ybWFsIEphdmFTY3JpcHQgZG9lcyBub3Qgd29yayBhc1xuICogZXhwZWN0ZWQgZHVyaW5nIGJvb3RzdHJhcHBpbmcgKHNlZSBtaXJyb3IuanMgaW4gcjExNDkwMykuXG4gKlxuICogQHBhcmFtIHtmdW5jdGlvbn0gY3RvciBDb25zdHJ1Y3RvciBmdW5jdGlvbiB3aGljaCBuZWVkcyB0byBpbmhlcml0IHRoZVxuICogICAgIHByb3RvdHlwZS5cbiAqIEBwYXJhbSB7ZnVuY3Rpb259IHN1cGVyQ3RvciBDb25zdHJ1Y3RvciBmdW5jdGlvbiB0byBpbmhlcml0IHByb3RvdHlwZSBmcm9tLlxuICovXG5leHBvcnRzLmluaGVyaXRzID0gcmVxdWlyZSgnaW5oZXJpdHMnKTtcblxuZXhwb3J0cy5fZXh0ZW5kID0gZnVuY3Rpb24ob3JpZ2luLCBhZGQpIHtcbiAgLy8gRG9uJ3QgZG8gYW55dGhpbmcgaWYgYWRkIGlzbid0IGFuIG9iamVjdFxuICBpZiAoIWFkZCB8fCAhaXNPYmplY3QoYWRkKSkgcmV0dXJuIG9yaWdpbjtcblxuICB2YXIga2V5cyA9IE9iamVjdC5rZXlzKGFkZCk7XG4gIHZhciBpID0ga2V5cy5sZW5ndGg7XG4gIHdoaWxlIChpLS0pIHtcbiAgICBvcmlnaW5ba2V5c1tpXV0gPSBhZGRba2V5c1tpXV07XG4gIH1cbiAgcmV0dXJuIG9yaWdpbjtcbn07XG5cbmZ1bmN0aW9uIGhhc093blByb3BlcnR5KG9iaiwgcHJvcCkge1xuICByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iaiwgcHJvcCk7XG59XG4iLCIvKipcclxuICogQ3JlYXRlZCBieSByaWNoYXJkLmxpdmluZ3N0b24gb24gMTgvMDIvMjAxNy5cclxuICovXHJcbid1c2Ugc3RyaWN0JztcclxuXHJcbnZhciB1dGlsID0gcmVxdWlyZSgndXRpbCcpLFxyXG5cdEVFID0gcmVxdWlyZSgnZXZlbnRzJykuRXZlbnRFbWl0dGVyO1xyXG5cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gSlNPTlZpZXc7XHJcbnV0aWwuaW5oZXJpdHMoSlNPTlZpZXcsIEVFKTtcclxuXHJcblxyXG5mdW5jdGlvbiBKU09OVmlldyhuYW1lXywgdmFsdWVfKXtcclxuXHR2YXIgc2VsZiA9IHRoaXM7XHJcblxyXG5cdEVFLmNhbGwoc2VsZik7XHJcblxyXG5cdGlmKGFyZ3VtZW50cy5sZW5ndGggPCAyKXtcclxuXHRcdHZhbHVlXyA9IG5hbWVfO1xyXG5cdFx0bmFtZV8gPSB1bmRlZmluZWQ7XHJcblx0fVxyXG5cclxuXHR2YXIgbmFtZSwgdmFsdWUsIHR5cGUsXHJcblx0XHRkb21FdmVudExpc3RlbmVycyA9IFtdLCBjaGlsZHJlbiA9IFtdLCBleHBhbmRlZCA9IGZhbHNlLFxyXG5cdFx0ZWRpdHRpbmdOYW1lID0gZmFsc2UsIGVkaXR0aW5nVmFsdWUgPSBmYWxzZSxcclxuXHRcdG5hbWVFZGl0YWJsZSA9IHRydWUsIHZhbHVlRWRpdGFibGUgPSB0cnVlO1xyXG5cclxuXHR2YXIgZG9tID0ge1xyXG5cdFx0Y29udGFpbmVyIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHRjb2xsYXBzZUV4cGFuZCA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0bmFtZSA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0c2VwYXJhdG9yIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHR2YWx1ZSA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0ZGVsZXRlIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHRjaGlsZHJlbiA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0aW5zZXJ0IDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JylcclxuXHR9O1xyXG5cclxuXHJcblx0T2JqZWN0LmRlZmluZVByb3BlcnRpZXMoc2VsZiwge1xyXG5cclxuXHRcdGRvbSA6IHtcclxuXHRcdFx0dmFsdWUgOiBkb20uY29udGFpbmVyLFxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fSxcclxuXHJcblx0XHRuYW1lIDoge1xyXG5cdFx0XHRnZXQgOiBmdW5jdGlvbigpe1xyXG5cdFx0XHRcdHJldHVybiBuYW1lO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0c2V0IDogc2V0TmFtZSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0dmFsdWUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIHZhbHVlO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0c2V0IDogc2V0VmFsdWUsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdHR5cGUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIHR5cGU7XHJcblx0XHRcdH0sXHJcblxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fSxcclxuXHJcblx0XHRuYW1lRWRpdGFibGUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIG5hbWVFZGl0YWJsZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdHNldCA6IGZ1bmN0aW9uKHZhbHVlKXtcclxuXHRcdFx0XHRuYW1lRWRpdGFibGUgPSAhIXZhbHVlO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0dmFsdWVFZGl0YWJsZSA6IHtcclxuXHRcdFx0Z2V0IDogZnVuY3Rpb24oKXtcclxuXHRcdFx0XHRyZXR1cm4gdmFsdWVFZGl0YWJsZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdHNldCA6IGZ1bmN0aW9uKHZhbHVlKXtcclxuXHRcdFx0XHR2YWx1ZUVkaXRhYmxlID0gISF2YWx1ZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdHJlZnJlc2ggOiB7XHJcblx0XHRcdHZhbHVlIDogcmVmcmVzaCxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0Y29sbGFwc2UgOiB7XHJcblx0XHRcdHZhbHVlIDogY29sbGFwc2UsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdGV4cGFuZCA6IHtcclxuXHRcdFx0dmFsdWUgOiBleHBhbmQsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdGRlc3Ryb3kgOiB7XHJcblx0XHRcdHZhbHVlIDogZGVzdHJveSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0ZWRpdE5hbWUgOiB7XHJcblx0XHRcdHZhbHVlIDogZWRpdEZpZWxkLmJpbmQobnVsbCwgJ25hbWUnKSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0ZWRpdFZhbHVlIDoge1xyXG5cdFx0XHR2YWx1ZSA6IGVkaXRGaWVsZC5iaW5kKG51bGwsICd2YWx1ZScpLFxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fVxyXG5cclxuXHR9KTtcclxuXHJcblxyXG5cdE9iamVjdC5rZXlzKGRvbSkuZm9yRWFjaChmdW5jdGlvbihrKXtcclxuXHRcdHZhciBlbGVtZW50ID0gZG9tW2tdO1xyXG5cclxuXHRcdGlmKGsgPT0gJ2NvbnRhaW5lcicpe1xyXG5cdFx0XHRyZXR1cm47XHJcblx0XHR9XHJcblxyXG5cdFx0ZWxlbWVudC5jbGFzc05hbWUgPSBrO1xyXG5cdFx0ZG9tLmNvbnRhaW5lci5hcHBlbmRDaGlsZChlbGVtZW50KTtcclxuXHR9KTtcclxuXHJcblx0ZG9tLmNvbnRhaW5lci5jbGFzc05hbWUgPSAnanNvblZpZXcnO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5jb2xsYXBzZUV4cGFuZCwgJ2NsaWNrJywgb25Db2xsYXBzZUV4cGFuZENsaWNrKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2NsaWNrJywgZXhwYW5kLmJpbmQobnVsbCwgZmFsc2UpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnY2xpY2snLCBleHBhbmQuYmluZChudWxsLCBmYWxzZSkpO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnZGJsY2xpY2snLCBlZGl0RmllbGQuYmluZChudWxsLCAnbmFtZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnYmx1cicsIGVkaXRGaWVsZFN0b3AuYmluZChudWxsLCAnbmFtZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAna2V5cHJlc3MnLCBlZGl0RmllbGRLZXlQcmVzc2VkLmJpbmQobnVsbCwgJ25hbWUnKSk7XHJcblx0YWRkRG9tRXZlbnRMaXN0ZW5lcihkb20ubmFtZSwgJ2tleWRvd24nLCBlZGl0RmllbGRUYWJQcmVzc2VkLmJpbmQobnVsbCwgJ25hbWUnKSk7XHJcblxyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLnZhbHVlLCAnZGJsY2xpY2snLCBlZGl0RmllbGQuYmluZChudWxsLCAndmFsdWUnKSk7XHJcblx0YWRkRG9tRXZlbnRMaXN0ZW5lcihkb20udmFsdWUsICdibHVyJywgZWRpdEZpZWxkU3RvcC5iaW5kKG51bGwsICd2YWx1ZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2tleXByZXNzJywgZWRpdEZpZWxkS2V5UHJlc3NlZC5iaW5kKG51bGwsICd2YWx1ZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2tleWRvd24nLCBlZGl0RmllbGRUYWJQcmVzc2VkLmJpbmQobnVsbCwgJ3ZhbHVlJykpO1xyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLnZhbHVlLCAna2V5ZG93bicsIG51bWVyaWNWYWx1ZUtleURvd24pO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5pbnNlcnQsICdjbGljaycsIG9uSW5zZXJ0Q2xpY2spO1xyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLmRlbGV0ZSwgJ2NsaWNrJywgb25EZWxldGVDbGljayk7XHJcblxyXG5cdHNldE5hbWUobmFtZV8pO1xyXG5cdHNldFZhbHVlKHZhbHVlXyk7XHJcblxyXG5cclxuXHRmdW5jdGlvbiByZWZyZXNoKCl7XHJcblx0XHR2YXIgZXhwYW5kYWJsZSA9IHR5cGUgPT0gJ29iamVjdCcgfHwgdHlwZSA9PSAnYXJyYXknO1xyXG5cclxuXHRcdGNoaWxkcmVuLmZvckVhY2goZnVuY3Rpb24oY2hpbGQpe1xyXG5cdFx0XHRjaGlsZC5yZWZyZXNoKCk7XHJcblx0XHR9KTtcclxuXHJcblx0XHRkb20uY29sbGFwc2VFeHBhbmQuc3R5bGUuZGlzcGxheSA9IGV4cGFuZGFibGUgPyAnJyA6ICdub25lJztcclxuXHJcblx0XHRpZihleHBhbmRlZCAmJiBleHBhbmRhYmxlKXtcclxuXHRcdFx0ZXhwYW5kKCk7XHJcblx0XHR9XHJcblx0XHRlbHNle1xyXG5cdFx0XHRjb2xsYXBzZSgpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGNvbGxhcHNlKHJlY3Vyc2l2ZSl7XHJcblx0XHRpZihyZWN1cnNpdmUpe1xyXG5cdFx0XHRjaGlsZHJlbi5mb3JFYWNoKGZ1bmN0aW9uKGNoaWxkKXtcclxuXHRcdFx0XHRjaGlsZC5jb2xsYXBzZSh0cnVlKTtcclxuXHRcdFx0fSk7XHJcblx0XHR9XHJcblxyXG5cdFx0ZXhwYW5kZWQgPSBmYWxzZTtcclxuXHJcblx0XHRkb20uY2hpbGRyZW4uc3R5bGUuZGlzcGxheSA9ICdub25lJztcclxuXHRcdGRvbS5jb2xsYXBzZUV4cGFuZC5jbGFzc05hbWUgPSAnZXhwYW5kJztcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LmFkZCgnY29sbGFwc2VkJyk7XHJcblx0XHRkb20uY29udGFpbmVyLmNsYXNzTGlzdC5yZW1vdmUoJ2V4cGFuZGVkJyk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZXhwYW5kKHJlY3Vyc2l2ZSl7XHJcblx0XHR2YXIga2V5cztcclxuXHJcblx0XHRpZih0eXBlID09ICdvYmplY3QnKXtcclxuXHRcdFx0a2V5cyA9IE9iamVjdC5rZXlzKHZhbHVlKTtcclxuXHRcdH1cclxuXHRcdGVsc2UgaWYodHlwZSA9PSAnYXJyYXknKXtcclxuXHRcdFx0a2V5cyA9IHZhbHVlLm1hcChmdW5jdGlvbih2LCBrKXtcclxuXHRcdFx0XHRyZXR1cm4gaztcclxuXHRcdFx0fSk7XHJcblx0XHR9XHJcblx0XHRlbHNle1xyXG5cdFx0XHRrZXlzID0gW107XHJcblx0XHR9XHJcblxyXG5cdFx0Ly8gUmVtb3ZlIGNoaWxkcmVuIHRoYXQgbm8gbG9uZ2VyIGV4aXN0XHJcblx0XHRmb3IodmFyIGkgPSBjaGlsZHJlbi5sZW5ndGggLSAxOyBpID49IDA7IGkgLS0pe1xyXG5cdFx0XHR2YXIgY2hpbGQgPSBjaGlsZHJlbltpXTtcclxuXHJcblx0XHRcdGlmKGtleXMuaW5kZXhPZihjaGlsZC5uYW1lKSA9PSAtMSl7XHJcblx0XHRcdFx0Y2hpbGRyZW4uc3BsaWNlKGksIDEpO1xyXG5cdFx0XHRcdHJlbW92ZUNoaWxkKGNoaWxkKTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cclxuXHRcdGlmKHR5cGUgIT0gJ29iamVjdCcgJiYgdHlwZSAhPSAnYXJyYXknKXtcclxuXHRcdFx0cmV0dXJuIGNvbGxhcHNlKCk7XHJcblx0XHR9XHJcblxyXG5cdFx0a2V5cy5mb3JFYWNoKGZ1bmN0aW9uKGtleSl7XHJcblx0XHRcdGFkZENoaWxkKGtleSwgdmFsdWVba2V5XSk7XHJcblx0XHR9KTtcclxuXHJcblx0XHRpZihyZWN1cnNpdmUpe1xyXG5cdFx0XHRjaGlsZHJlbi5mb3JFYWNoKGZ1bmN0aW9uKGNoaWxkKXtcclxuXHRcdFx0XHRjaGlsZC5leHBhbmQodHJ1ZSk7XHJcblx0XHRcdH0pO1xyXG5cdFx0fVxyXG5cclxuXHRcdGV4cGFuZGVkID0gdHJ1ZTtcclxuXHRcdGRvbS5jaGlsZHJlbi5zdHlsZS5kaXNwbGF5ID0gJyc7XHJcblx0XHRkb20uY29sbGFwc2VFeHBhbmQuY2xhc3NOYW1lID0gJ2NvbGxhcHNlJztcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LmFkZCgnZXhwYW5kZWQnKTtcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LnJlbW92ZSgnY29sbGFwc2VkJyk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZGVzdHJveSgpe1xyXG5cdFx0dmFyIGNoaWxkLCBldmVudDtcclxuXHJcblx0XHR3aGlsZShldmVudCA9IGRvbUV2ZW50TGlzdGVuZXJzLnBvcCgpKXtcclxuXHRcdFx0ZXZlbnQuZWxlbWVudC5yZW1vdmVFdmVudExpc3RlbmVyKGV2ZW50Lm5hbWUsIGV2ZW50LmZuKTtcclxuXHRcdH1cclxuXHJcblx0XHR3aGlsZShjaGlsZCA9IGNoaWxkcmVuLnBvcCgpKXtcclxuXHRcdFx0cmVtb3ZlQ2hpbGQoY2hpbGQpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIHNldE5hbWUobmV3TmFtZSl7XHJcblx0XHR2YXIgbmFtZVR5cGUgPSB0eXBlb2YgbmV3TmFtZSxcclxuXHRcdFx0b2xkTmFtZSA9IG5hbWU7XHJcblxyXG5cdFx0aWYobmV3TmFtZSA9PT0gbmFtZSl7XHJcblx0XHRcdHJldHVybjtcclxuXHRcdH1cclxuXHJcblx0XHRpZihuYW1lVHlwZSAhPSAnc3RyaW5nJyAmJiBuYW1lVHlwZSAhPSAnbnVtYmVyJyl7XHJcblx0XHRcdHRocm93IG5ldyBFcnJvcignTmFtZSBtdXN0IGJlIGVpdGhlciBzdHJpbmcgb3IgbnVtYmVyLCAnICsgbmV3TmFtZSk7XHJcblx0XHR9XHJcblxyXG5cdFx0ZG9tLm5hbWUuaW5uZXJUZXh0ID0gbmV3TmFtZTtcclxuXHRcdG5hbWUgPSBuZXdOYW1lO1xyXG5cdFx0c2VsZi5lbWl0KCdyZW5hbWUnLCBzZWxmLCBvbGROYW1lLCBuZXdOYW1lKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBzZXRWYWx1ZShuZXdWYWx1ZSl7XHJcblx0XHR2YXIgb2xkVmFsdWUgPSB2YWx1ZSxcclxuXHRcdFx0c3RyO1xyXG5cclxuXHRcdHR5cGUgPSBnZXRUeXBlKG5ld1ZhbHVlKTtcclxuXHJcblx0XHRzd2l0Y2godHlwZSl7XHJcblx0XHRcdGNhc2UgJ251bGwnOlxyXG5cdFx0XHRcdHN0ciA9ICdudWxsJztcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdFx0Y2FzZSAnb2JqZWN0JzpcclxuXHRcdFx0XHRzdHIgPSAnT2JqZWN0WycgKyBPYmplY3Qua2V5cyhuZXdWYWx1ZSkubGVuZ3RoICsgJ10nO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cclxuXHRcdFx0Y2FzZSAnYXJyYXknOlxyXG5cdFx0XHRcdHN0ciA9ICdBcnJheVsnICsgbmV3VmFsdWUubGVuZ3RoICsgJ10nO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cclxuXHRcdFx0ZGVmYXVsdDpcclxuXHRcdFx0XHRzdHIgPSBuZXdWYWx1ZTtcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdH1cclxuXHJcblx0XHRkb20udmFsdWUuaW5uZXJUZXh0ID0gc3RyO1xyXG5cdFx0ZG9tLnZhbHVlLmNsYXNzTmFtZSA9ICd2YWx1ZSAnICsgdHlwZTtcclxuXHJcblx0XHRpZihuZXdWYWx1ZSA9PT0gdmFsdWUpe1xyXG5cdFx0XHRyZXR1cm47XHJcblx0XHR9XHJcblxyXG5cdFx0dmFsdWUgPSBuZXdWYWx1ZTtcclxuXHJcblx0XHRpZih0eXBlID09ICdhcnJheScgfHwgdHlwZSA9PSAnb2JqZWN0Jyl7XHJcblx0XHRcdC8vIENhbm5vdCBlZGl0IG9iamVjdHMgYXMgc3RyaW5nIGJlY2F1c2UgdGhlIGZvcm1hdHRpbmcgaXMgdG9vIG1lc3N5XHJcblx0XHRcdC8vIFdvdWxkIGhhdmUgdG8gZWl0aGVyIHBhc3MgYXMgSlNPTiBhbmQgZm9yY2UgdXNlciB0byB3cmFwIHByb3BlcnRpZXMgaW4gcXVvdGVzXHJcblx0XHRcdC8vIE9yIGZpcnN0IEpTT04gc3RyaW5naWZ5IHRoZSBpbnB1dCBiZWZvcmUgcGFzc2luZywgdGhpcyBjb3VsZCBhbGxvdyB1c2VycyB0byByZWZlcmVuY2UgZ2xvYmFsc1xyXG5cclxuXHRcdFx0Ly8gSW5zdGVhZCB0aGUgdXNlciBjYW4gbW9kaWZ5IGluZGl2aWR1YWwgcHJvcGVydGllcywgb3IganVzdCBkZWxldGUgdGhlIG9iamVjdCBhbmQgc3RhcnQgYWdhaW5cclxuXHRcdFx0dmFsdWVFZGl0YWJsZSA9IGZhbHNlO1xyXG5cclxuXHRcdFx0aWYodHlwZSA9PSAnYXJyYXknKXtcclxuXHRcdFx0XHQvLyBPYnZpb3VzbHkgY2Fubm90IG1vZGlmeSBhcnJheSBrZXlzXHJcblx0XHRcdFx0bmFtZUVkaXRhYmxlID0gZmFsc2U7XHJcblx0XHRcdH1cclxuXHRcdH1cclxuXHJcblx0XHRyZWZyZXNoKCk7XHJcblx0XHRzZWxmLmVtaXQoJ2NoYW5nZScsIG5hbWUsIG9sZFZhbHVlLCBuZXdWYWx1ZSk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gYWRkQ2hpbGQoa2V5LCB2YWwpe1xyXG5cdFx0dmFyIGNoaWxkO1xyXG5cclxuXHRcdGZvcih2YXIgaSA9IDAsIGxlbiA9IGNoaWxkcmVuLmxlbmd0aDsgaSA8IGxlbjsgaSArKyl7XHJcblx0XHRcdGlmKGNoaWxkcmVuW2ldLm5hbWUgPT0ga2V5KXtcclxuXHRcdFx0XHRjaGlsZCA9IGNoaWxkcmVuW2ldO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cdFx0XHR9XHJcblx0XHR9XHJcblxyXG5cdFx0aWYoY2hpbGQpe1xyXG5cdFx0XHRjaGlsZC52YWx1ZSA9IHZhbDtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdGNoaWxkID0gbmV3IEpTT05WaWV3KGtleSwgdmFsKTtcclxuXHRcdFx0Y2hpbGQub25jZSgncmVuYW1lJywgb25DaGlsZFJlbmFtZSk7XHJcblx0XHRcdGNoaWxkLm9uKCdkZWxldGUnLCBvbkNoaWxkRGVsZXRlKTtcclxuXHRcdFx0Y2hpbGQub24oJ2NoYW5nZScsIG9uQ2hpbGRDaGFuZ2UpO1xyXG5cdFx0XHRjaGlsZHJlbi5wdXNoKGNoaWxkKTtcclxuXHRcdH1cclxuXHJcblx0XHRkb20uY2hpbGRyZW4uYXBwZW5kQ2hpbGQoY2hpbGQuZG9tKTtcclxuXHJcblx0XHRyZXR1cm4gY2hpbGQ7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gcmVtb3ZlQ2hpbGQoY2hpbGQpe1xyXG5cdFx0aWYoY2hpbGQuZG9tLnBhcmVudE5vZGUpe1xyXG5cdFx0XHRkb20uY2hpbGRyZW4ucmVtb3ZlQ2hpbGQoY2hpbGQuZG9tKTtcclxuXHRcdH1cclxuXHJcblx0XHRjaGlsZC5kZXN0cm95KCk7XHJcblx0XHRjaGlsZC5yZW1vdmVBbGxMaXN0ZW5lcnMoKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBlZGl0RmllbGQoZmllbGQpe1xyXG5cdFx0dmFyIGVkaXRhYmxlID0gZmllbGQgPT0gJ25hbWUnID8gbmFtZUVkaXRhYmxlIDogdmFsdWVFZGl0YWJsZSxcclxuXHRcdFx0ZWxlbWVudCA9IGRvbVtmaWVsZF07XHJcblxyXG5cdFx0aWYoIWVkaXRhYmxlKXtcclxuXHRcdFx0cmV0dXJuO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGZpZWxkID09ICd2YWx1ZScgJiYgdHlwZSA9PSAnc3RyaW5nJyl7XHJcblx0XHRcdGVsZW1lbnQuaW5uZXJUZXh0ID0gJ1wiJyArIHZhbHVlICsgJ1wiJztcclxuXHRcdH1cclxuXHJcblx0XHRpZihmaWVsZCA9PSAnbmFtZScpe1xyXG5cdFx0XHRlZGl0dGluZ05hbWUgPSB0cnVlO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGZpZWxkID09ICd2YWx1ZScpe1xyXG5cdFx0XHRlZGl0dGluZ1ZhbHVlID0gdHJ1ZTtcclxuXHRcdH1cclxuXHJcblx0XHRlbGVtZW50LmNsYXNzTGlzdC5hZGQoJ2VkaXQnKTtcclxuXHRcdGVsZW1lbnQuc2V0QXR0cmlidXRlKCdjb250ZW50ZWRpdGFibGUnLCB0cnVlKTtcclxuXHRcdGVsZW1lbnQuZm9jdXMoKTtcclxuXHRcdGRvY3VtZW50LmV4ZWNDb21tYW5kKCdzZWxlY3RBbGwnLCBmYWxzZSwgbnVsbCk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZWRpdEZpZWxkU3RvcChmaWVsZCl7XHJcblx0XHR2YXIgZWxlbWVudCA9IGRvbVtmaWVsZF07XHJcblx0XHRcclxuXHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdGlmKCFlZGl0dGluZ05hbWUpe1xyXG5cdFx0XHRcdHJldHVybjtcclxuXHRcdFx0fVxyXG5cdFx0XHRlZGl0dGluZ05hbWUgPSBmYWxzZTtcclxuXHRcdH1cclxuXHJcblx0XHRpZihmaWVsZCA9PSAndmFsdWUnKXtcclxuXHRcdFx0aWYoIWVkaXR0aW5nVmFsdWUpe1xyXG5cdFx0XHRcdHJldHVybjtcclxuXHRcdFx0fVxyXG5cdFx0XHRlZGl0dGluZ1ZhbHVlID0gZmFsc2U7XHJcblx0XHR9XHJcblx0XHRcclxuXHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdHNldE5hbWUoZWxlbWVudC5pbm5lclRleHQpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0dHJ5e1xyXG5cdFx0XHRcdHNldFZhbHVlKEpTT04ucGFyc2UoZWxlbWVudC5pbm5lclRleHQpKTtcclxuXHRcdFx0fVxyXG5cdFx0XHRjYXRjaChlcnIpe1xyXG5cdFx0XHRcdHNldFZhbHVlKGVsZW1lbnQuaW5uZXJUZXh0KTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cclxuXHRcdGVsZW1lbnQuY2xhc3NMaXN0LnJlbW92ZSgnZWRpdCcpO1xyXG5cdFx0ZWxlbWVudC5yZW1vdmVBdHRyaWJ1dGUoJ2NvbnRlbnRlZGl0YWJsZScpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGVkaXRGaWVsZEtleVByZXNzZWQoZmllbGQsIGUpe1xyXG5cdFx0c3dpdGNoKGUua2V5KXtcclxuXHRcdFx0Y2FzZSAnRXNjYXBlJzpcclxuXHRcdFx0Y2FzZSAnRW50ZXInOlxyXG5cdFx0XHRcdGVkaXRGaWVsZFN0b3AoZmllbGQpO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGVkaXRGaWVsZFRhYlByZXNzZWQoZmllbGQsIGUpe1xyXG5cdFx0aWYoZS5rZXkgPT0gJ1RhYicpe1xyXG5cdFx0XHRlZGl0RmllbGRTdG9wKGZpZWxkKTtcclxuXHJcblx0XHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdFx0ZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cdFx0XHRcdGVkaXRGaWVsZCgndmFsdWUnKTtcclxuXHRcdFx0fVxyXG5cdFx0XHRlbHNle1xyXG5cdFx0XHRcdGVkaXRGaWVsZFN0b3AoZmllbGQpO1xyXG5cdFx0XHR9XHJcblx0XHR9XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gbnVtZXJpY1ZhbHVlS2V5RG93bihlKXtcclxuXHRcdHZhciBpbmNyZW1lbnQgPSAwLCBjdXJyZW50VmFsdWU7XHJcblxyXG5cdFx0aWYodHlwZSAhPSAnbnVtYmVyJyl7XHJcblx0XHRcdHJldHVybjtcclxuXHRcdH1cclxuXHJcblx0XHRzd2l0Y2goZS5rZXkpe1xyXG5cdFx0XHRjYXNlICdBcnJvd0Rvd24nOlxyXG5cdFx0XHRjYXNlICdEb3duJzpcclxuXHRcdFx0XHRpbmNyZW1lbnQgPSAtMTtcclxuXHRcdFx0XHRicmVhaztcclxuXHJcblx0XHRcdGNhc2UgJ0Fycm93VXAnOlxyXG5cdFx0XHRjYXNlICdVcCc6XHJcblx0XHRcdFx0aW5jcmVtZW50ID0gMTtcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdH1cclxuXHJcblx0XHRpZihlLnNoaWZ0S2V5KXtcclxuXHRcdFx0aW5jcmVtZW50ICo9IDEwO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGUuY3RybEtleSB8fCBlLm1ldGFLZXkpe1xyXG5cdFx0XHRpbmNyZW1lbnQgLz0gMTA7XHJcblx0XHR9XHJcblxyXG5cdFx0aWYoaW5jcmVtZW50KXtcclxuXHRcdFx0Y3VycmVudFZhbHVlID0gcGFyc2VGbG9hdChkb20udmFsdWUuaW5uZXJUZXh0KTtcclxuXHJcblx0XHRcdGlmKCFpc05hTihjdXJyZW50VmFsdWUpKXtcclxuXHRcdFx0XHRkb20udmFsdWUuaW5uZXJUZXh0ID0gTnVtYmVyKChjdXJyZW50VmFsdWUgKyBpbmNyZW1lbnQpLnRvRml4ZWQoMTApKTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGdldFR5cGUodmFsdWUpe1xyXG5cdFx0dmFyIHR5cGUgPSB0eXBlb2YgdmFsdWU7XHJcblxyXG5cdFx0aWYodHlwZSA9PSAnb2JqZWN0Jyl7XHJcblx0XHRcdGlmKHZhbHVlID09PSBudWxsKXtcclxuXHRcdFx0XHRyZXR1cm4gJ251bGwnO1xyXG5cdFx0XHR9XHJcblxyXG5cdFx0XHRpZihBcnJheS5pc0FycmF5KHZhbHVlKSl7XHJcblx0XHRcdFx0cmV0dXJuICdhcnJheSc7XHJcblx0XHRcdH1cclxuXHRcdH1cclxuXHJcblx0XHRyZXR1cm4gdHlwZTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkNvbGxhcHNlRXhwYW5kQ2xpY2soKXtcclxuXHRcdGlmKGV4cGFuZGVkKXtcclxuXHRcdFx0Y29sbGFwc2UoKTtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdGV4cGFuZCgpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIG9uSW5zZXJ0Q2xpY2soKXtcclxuXHRcdHZhciBuZXdOYW1lID0gdHlwZSA9PSAnYXJyYXknID8gdmFsdWUubGVuZ3RoIDogdW5kZWZpbmVkLFxyXG5cdFx0XHRjaGlsZCA9IGFkZENoaWxkKG5ld05hbWUsIG51bGwpO1xyXG5cclxuXHRcdGlmKHR5cGUgPT0gJ2FycmF5Jyl7XHJcblx0XHRcdHZhbHVlLnB1c2gobnVsbCk7XHJcblx0XHRcdGNoaWxkLmVkaXRWYWx1ZSgpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0Y2hpbGQuZWRpdE5hbWUoKTtcclxuXHRcdH1cclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkRlbGV0ZUNsaWNrKCl7XHJcblx0XHRzZWxmLmVtaXQoJ2RlbGV0ZScsIHNlbGYpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIG9uQ2hpbGRSZW5hbWUoY2hpbGQsIG9sZE5hbWUsIG5ld05hbWUpe1xyXG5cdFx0dmFyIGFsbG93ID0gbmV3TmFtZSAmJiB0eXBlICE9ICdhcnJheScgJiYgIShuZXdOYW1lIGluIHZhbHVlKTtcclxuXHJcblx0XHRpZihhbGxvdyl7XHJcblx0XHRcdHZhbHVlW25ld05hbWVdID0gY2hpbGQudmFsdWU7XHJcblx0XHRcdGRlbGV0ZSB2YWx1ZVtvbGROYW1lXTtcclxuXHRcdH1cclxuXHRcdGVsc2UgaWYob2xkTmFtZSA9PT0gdW5kZWZpbmVkKXtcclxuXHRcdFx0Ly8gQSBuZXcgbm9kZSBpbnNlcnRlZCB2aWEgdGhlIFVJXHJcblx0XHRcdHJlbW92ZUNoaWxkKGNoaWxkKTtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdC8vIENhbm5vdCByZW5hbWUgYXJyYXkga2V5cywgb3IgZHVwbGljYXRlIG9iamVjdCBrZXkgbmFtZXNcclxuXHRcdFx0Y2hpbGQubmFtZSA9IG9sZE5hbWU7XHJcblx0XHR9XHJcblxyXG5cdFx0Y2hpbGQub25jZSgncmVuYW1lJywgb25DaGlsZFJlbmFtZSk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gb25DaGlsZENoYW5nZShrZXlQYXRoLCBvbGRWYWx1ZSwgbmV3VmFsdWUsIHJlY3Vyc2VkKXtcclxuXHRcdGlmKCFyZWN1cnNlZCl7XHJcblx0XHRcdHZhbHVlW2tleVBhdGhdID0gbmV3VmFsdWU7XHJcblx0XHR9XHJcblxyXG5cdFx0c2VsZi5lbWl0KCdjaGFuZ2UnLCBuYW1lICsgJy4nICsga2V5UGF0aCwgb2xkVmFsdWUsIG5ld1ZhbHVlLCB0cnVlKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkNoaWxkRGVsZXRlKGNoaWxkKXtcclxuXHRcdHZhciBrZXkgPSBjaGlsZC5uYW1lO1xyXG5cclxuXHRcdGlmKHR5cGUgPT0gJ2FycmF5Jyl7XHJcblx0XHRcdHZhbHVlLnNwbGljZShrZXksIDEpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0ZGVsZXRlIHZhbHVlW2tleV07XHJcblx0XHR9XHJcblxyXG5cdFx0cmVmcmVzaCgpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGFkZERvbUV2ZW50TGlzdGVuZXIoZWxlbWVudCwgbmFtZSwgZm4pe1xyXG5cdFx0ZWxlbWVudC5hZGRFdmVudExpc3RlbmVyKG5hbWUsIGZuKTtcclxuXHRcdGRvbUV2ZW50TGlzdGVuZXJzLnB1c2goe2VsZW1lbnQgOiBlbGVtZW50LCBuYW1lIDogbmFtZSwgZm4gOiBmbn0pO1xyXG5cdH1cclxufSJdfQ== \ No newline at end of file + //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJpbmRleC5qcyIsIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy9ldmVudHMvZXZlbnRzLmpzIiwibm9kZV9tb2R1bGVzL2Jyb3dzZXJpZnkvbm9kZV9tb2R1bGVzL3Byb2Nlc3MvYnJvd3Nlci5qcyIsIm5vZGVfbW9kdWxlcy9icm93c2VyaWZ5L25vZGVfbW9kdWxlcy91dGlsL25vZGVfbW9kdWxlcy9pbmhlcml0cy9pbmhlcml0c19icm93c2VyLmpzIiwibm9kZV9tb2R1bGVzL2Jyb3dzZXJpZnkvbm9kZV9tb2R1bGVzL3V0aWwvc3VwcG9ydC9pc0J1ZmZlckJyb3dzZXIuanMiLCJub2RlX21vZHVsZXMvYnJvd3NlcmlmeS9ub2RlX21vZHVsZXMvdXRpbC91dGlsLmpzIiwibm9kZV9tb2R1bGVzL2pzb24tdmlldy9KU09OVmlldy5qcyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTtBQ0FBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3hCQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDOVNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ3BMQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDdkJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7O0FDTEE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQzFrQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIi8qKlxyXG4gKiBDcmVhdGVkIGJ5IHIxY2g0IG9uIDAyLzEwLzIwMTYuXHJcbiAqL1xyXG5cclxudmFyIEpTT05WaWV3ID0gcmVxdWlyZSgnanNvbi12aWV3Jyk7XHJcblxyXG52YXIgdmlldyA9IG5ldyBKU09OVmlldygnZXhhbXBsZScsIHtcclxuICAgIGhlbGxvIDogJ3dvcmxkJyxcclxuICAgIGRvdWJsZUNsaWNrIDogJ21lIHRvIGVkaXQnLFxyXG4gICAgYSA6IG51bGwsXHJcbiAgICBiIDogdHJ1ZSxcclxuICAgIGMgOiBmYWxzZSxcclxuICAgIGQgOiAxLFxyXG4gICAgZSA6IHtuZXN0ZWQgOiAnb2JqZWN0J30sXHJcbiAgICBmIDogWzEsMiwzXVxyXG59KTtcclxuXHJcbnZpZXcub24oJ2NoYW5nZScsIGZ1bmN0aW9uKGtleSwgb2xkVmFsdWUsIG5ld1ZhbHVlKXtcclxuICAgIGNvbnNvbGUubG9nKCdjaGFuZ2UnLCBrZXksIG9sZFZhbHVlLCAnPT4nLCBuZXdWYWx1ZSk7XHJcbn0pO1xyXG5cclxudmlldy5leHBhbmQodHJ1ZSk7XHJcblxyXG5kb2N1bWVudC5ib2R5LmFwcGVuZENoaWxkKHZpZXcuZG9tKTtcclxud2luZG93LnZpZXcgPSB2aWV3OyIsIi8vIENvcHlyaWdodCBKb3llbnQsIEluYy4gYW5kIG90aGVyIE5vZGUgY29udHJpYnV0b3JzLlxuLy9cbi8vIFBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhXG4vLyBjb3B5IG9mIHRoaXMgc29mdHdhcmUgYW5kIGFzc29jaWF0ZWQgZG9jdW1lbnRhdGlvbiBmaWxlcyAodGhlXG4vLyBcIlNvZnR3YXJlXCIpLCB0byBkZWFsIGluIHRoZSBTb2Z0d2FyZSB3aXRob3V0IHJlc3RyaWN0aW9uLCBpbmNsdWRpbmdcbi8vIHdpdGhvdXQgbGltaXRhdGlvbiB0aGUgcmlnaHRzIHRvIHVzZSwgY29weSwgbW9kaWZ5LCBtZXJnZSwgcHVibGlzaCxcbi8vIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsIGNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXRcbi8vIHBlcnNvbnMgdG8gd2hvbSB0aGUgU29mdHdhcmUgaXMgZnVybmlzaGVkIHRvIGRvIHNvLCBzdWJqZWN0IHRvIHRoZVxuLy8gZm9sbG93aW5nIGNvbmRpdGlvbnM6XG4vL1xuLy8gVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWRcbi8vIGluIGFsbCBjb3BpZXMgb3Igc3Vic3RhbnRpYWwgcG9ydGlvbnMgb2YgdGhlIFNvZnR3YXJlLlxuLy9cbi8vIFRIRSBTT0ZUV0FSRSBJUyBQUk9WSURFRCBcIkFTIElTXCIsIFdJVEhPVVQgV0FSUkFOVFkgT0YgQU5ZIEtJTkQsIEVYUFJFU1Ncbi8vIE9SIElNUExJRUQsIElOQ0xVRElORyBCVVQgTk9UIExJTUlURUQgVE8gVEhFIFdBUlJBTlRJRVMgT0Zcbi8vIE1FUkNIQU5UQUJJTElUWSwgRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU5cbi8vIE5PIEVWRU5UIFNIQUxMIFRIRSBBVVRIT1JTIE9SIENPUFlSSUdIVCBIT0xERVJTIEJFIExJQUJMRSBGT1IgQU5ZIENMQUlNLFxuLy8gREFNQUdFUyBPUiBPVEhFUiBMSUFCSUxJVFksIFdIRVRIRVIgSU4gQU4gQUNUSU9OIE9GIENPTlRSQUNULCBUT1JUIE9SXG4vLyBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwgT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFXG4vLyBVU0UgT1IgT1RIRVIgREVBTElOR1MgSU4gVEhFIFNPRlRXQVJFLlxuXG5mdW5jdGlvbiBFdmVudEVtaXR0ZXIoKSB7XG4gIHRoaXMuX2V2ZW50cyA9IHRoaXMuX2V2ZW50cyB8fCB7fTtcbiAgdGhpcy5fbWF4TGlzdGVuZXJzID0gdGhpcy5fbWF4TGlzdGVuZXJzIHx8IHVuZGVmaW5lZDtcbn1cbm1vZHVsZS5leHBvcnRzID0gRXZlbnRFbWl0dGVyO1xuXG4vLyBCYWNrd2FyZHMtY29tcGF0IHdpdGggbm9kZSAwLjEwLnhcbkV2ZW50RW1pdHRlci5FdmVudEVtaXR0ZXIgPSBFdmVudEVtaXR0ZXI7XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX2V2ZW50cyA9IHVuZGVmaW5lZDtcbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuX21heExpc3RlbmVycyA9IHVuZGVmaW5lZDtcblxuLy8gQnkgZGVmYXVsdCBFdmVudEVtaXR0ZXJzIHdpbGwgcHJpbnQgYSB3YXJuaW5nIGlmIG1vcmUgdGhhbiAxMCBsaXN0ZW5lcnMgYXJlXG4vLyBhZGRlZCB0byBpdC4gVGhpcyBpcyBhIHVzZWZ1bCBkZWZhdWx0IHdoaWNoIGhlbHBzIGZpbmRpbmcgbWVtb3J5IGxlYWtzLlxuRXZlbnRFbWl0dGVyLmRlZmF1bHRNYXhMaXN0ZW5lcnMgPSAxMDtcblxuLy8gT2J2aW91c2x5IG5vdCBhbGwgRW1pdHRlcnMgc2hvdWxkIGJlIGxpbWl0ZWQgdG8gMTAuIFRoaXMgZnVuY3Rpb24gYWxsb3dzXG4vLyB0aGF0IHRvIGJlIGluY3JlYXNlZC4gU2V0IHRvIHplcm8gZm9yIHVubGltaXRlZC5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUuc2V0TWF4TGlzdGVuZXJzID0gZnVuY3Rpb24obikge1xuICBpZiAoIWlzTnVtYmVyKG4pIHx8IG4gPCAwIHx8IGlzTmFOKG4pKVxuICAgIHRocm93IFR5cGVFcnJvcignbiBtdXN0IGJlIGEgcG9zaXRpdmUgbnVtYmVyJyk7XG4gIHRoaXMuX21heExpc3RlbmVycyA9IG47XG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5lbWl0ID0gZnVuY3Rpb24odHlwZSkge1xuICB2YXIgZXIsIGhhbmRsZXIsIGxlbiwgYXJncywgaSwgbGlzdGVuZXJzO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzKVxuICAgIHRoaXMuX2V2ZW50cyA9IHt9O1xuXG4gIC8vIElmIHRoZXJlIGlzIG5vICdlcnJvcicgZXZlbnQgbGlzdGVuZXIgdGhlbiB0aHJvdy5cbiAgaWYgKHR5cGUgPT09ICdlcnJvcicpIHtcbiAgICBpZiAoIXRoaXMuX2V2ZW50cy5lcnJvciB8fFxuICAgICAgICAoaXNPYmplY3QodGhpcy5fZXZlbnRzLmVycm9yKSAmJiAhdGhpcy5fZXZlbnRzLmVycm9yLmxlbmd0aCkpIHtcbiAgICAgIGVyID0gYXJndW1lbnRzWzFdO1xuICAgICAgaWYgKGVyIGluc3RhbmNlb2YgRXJyb3IpIHtcbiAgICAgICAgdGhyb3cgZXI7IC8vIFVuaGFuZGxlZCAnZXJyb3InIGV2ZW50XG4gICAgICB9IGVsc2Uge1xuICAgICAgICAvLyBBdCBsZWFzdCBnaXZlIHNvbWUga2luZCBvZiBjb250ZXh0IHRvIHRoZSB1c2VyXG4gICAgICAgIHZhciBlcnIgPSBuZXcgRXJyb3IoJ1VuY2F1Z2h0LCB1bnNwZWNpZmllZCBcImVycm9yXCIgZXZlbnQuICgnICsgZXIgKyAnKScpO1xuICAgICAgICBlcnIuY29udGV4dCA9IGVyO1xuICAgICAgICB0aHJvdyBlcnI7XG4gICAgICB9XG4gICAgfVxuICB9XG5cbiAgaGFuZGxlciA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcblxuICBpZiAoaXNVbmRlZmluZWQoaGFuZGxlcikpXG4gICAgcmV0dXJuIGZhbHNlO1xuXG4gIGlmIChpc0Z1bmN0aW9uKGhhbmRsZXIpKSB7XG4gICAgc3dpdGNoIChhcmd1bWVudHMubGVuZ3RoKSB7XG4gICAgICAvLyBmYXN0IGNhc2VzXG4gICAgICBjYXNlIDE6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICBjYXNlIDI6XG4gICAgICAgIGhhbmRsZXIuY2FsbCh0aGlzLCBhcmd1bWVudHNbMV0pO1xuICAgICAgICBicmVhaztcbiAgICAgIGNhc2UgMzpcbiAgICAgICAgaGFuZGxlci5jYWxsKHRoaXMsIGFyZ3VtZW50c1sxXSwgYXJndW1lbnRzWzJdKTtcbiAgICAgICAgYnJlYWs7XG4gICAgICAvLyBzbG93ZXJcbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIGFyZ3MgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuICAgICAgICBoYW5kbGVyLmFwcGx5KHRoaXMsIGFyZ3MpO1xuICAgIH1cbiAgfSBlbHNlIGlmIChpc09iamVjdChoYW5kbGVyKSkge1xuICAgIGFyZ3MgPSBBcnJheS5wcm90b3R5cGUuc2xpY2UuY2FsbChhcmd1bWVudHMsIDEpO1xuICAgIGxpc3RlbmVycyA9IGhhbmRsZXIuc2xpY2UoKTtcbiAgICBsZW4gPSBsaXN0ZW5lcnMubGVuZ3RoO1xuICAgIGZvciAoaSA9IDA7IGkgPCBsZW47IGkrKylcbiAgICAgIGxpc3RlbmVyc1tpXS5hcHBseSh0aGlzLCBhcmdzKTtcbiAgfVxuXG4gIHJldHVybiB0cnVlO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5hZGRMaXN0ZW5lciA9IGZ1bmN0aW9uKHR5cGUsIGxpc3RlbmVyKSB7XG4gIHZhciBtO1xuXG4gIGlmICghaXNGdW5jdGlvbihsaXN0ZW5lcikpXG4gICAgdGhyb3cgVHlwZUVycm9yKCdsaXN0ZW5lciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcblxuICBpZiAoIXRoaXMuX2V2ZW50cylcbiAgICB0aGlzLl9ldmVudHMgPSB7fTtcblxuICAvLyBUbyBhdm9pZCByZWN1cnNpb24gaW4gdGhlIGNhc2UgdGhhdCB0eXBlID09PSBcIm5ld0xpc3RlbmVyXCIhIEJlZm9yZVxuICAvLyBhZGRpbmcgaXQgdG8gdGhlIGxpc3RlbmVycywgZmlyc3QgZW1pdCBcIm5ld0xpc3RlbmVyXCIuXG4gIGlmICh0aGlzLl9ldmVudHMubmV3TGlzdGVuZXIpXG4gICAgdGhpcy5lbWl0KCduZXdMaXN0ZW5lcicsIHR5cGUsXG4gICAgICAgICAgICAgIGlzRnVuY3Rpb24obGlzdGVuZXIubGlzdGVuZXIpID9cbiAgICAgICAgICAgICAgbGlzdGVuZXIubGlzdGVuZXIgOiBsaXN0ZW5lcik7XG5cbiAgaWYgKCF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgLy8gT3B0aW1pemUgdGhlIGNhc2Ugb2Ygb25lIGxpc3RlbmVyLiBEb24ndCBuZWVkIHRoZSBleHRyYSBhcnJheSBvYmplY3QuXG4gICAgdGhpcy5fZXZlbnRzW3R5cGVdID0gbGlzdGVuZXI7XG4gIGVsc2UgaWYgKGlzT2JqZWN0KHRoaXMuX2V2ZW50c1t0eXBlXSkpXG4gICAgLy8gSWYgd2UndmUgYWxyZWFkeSBnb3QgYW4gYXJyYXksIGp1c3QgYXBwZW5kLlxuICAgIHRoaXMuX2V2ZW50c1t0eXBlXS5wdXNoKGxpc3RlbmVyKTtcbiAgZWxzZVxuICAgIC8vIEFkZGluZyB0aGUgc2Vjb25kIGVsZW1lbnQsIG5lZWQgdG8gY2hhbmdlIHRvIGFycmF5LlxuICAgIHRoaXMuX2V2ZW50c1t0eXBlXSA9IFt0aGlzLl9ldmVudHNbdHlwZV0sIGxpc3RlbmVyXTtcblxuICAvLyBDaGVjayBmb3IgbGlzdGVuZXIgbGVha1xuICBpZiAoaXNPYmplY3QodGhpcy5fZXZlbnRzW3R5cGVdKSAmJiAhdGhpcy5fZXZlbnRzW3R5cGVdLndhcm5lZCkge1xuICAgIGlmICghaXNVbmRlZmluZWQodGhpcy5fbWF4TGlzdGVuZXJzKSkge1xuICAgICAgbSA9IHRoaXMuX21heExpc3RlbmVycztcbiAgICB9IGVsc2Uge1xuICAgICAgbSA9IEV2ZW50RW1pdHRlci5kZWZhdWx0TWF4TGlzdGVuZXJzO1xuICAgIH1cblxuICAgIGlmIChtICYmIG0gPiAwICYmIHRoaXMuX2V2ZW50c1t0eXBlXS5sZW5ndGggPiBtKSB7XG4gICAgICB0aGlzLl9ldmVudHNbdHlwZV0ud2FybmVkID0gdHJ1ZTtcbiAgICAgIGNvbnNvbGUuZXJyb3IoJyhub2RlKSB3YXJuaW5nOiBwb3NzaWJsZSBFdmVudEVtaXR0ZXIgbWVtb3J5ICcgK1xuICAgICAgICAgICAgICAgICAgICAnbGVhayBkZXRlY3RlZC4gJWQgbGlzdGVuZXJzIGFkZGVkLiAnICtcbiAgICAgICAgICAgICAgICAgICAgJ1VzZSBlbWl0dGVyLnNldE1heExpc3RlbmVycygpIHRvIGluY3JlYXNlIGxpbWl0LicsXG4gICAgICAgICAgICAgICAgICAgIHRoaXMuX2V2ZW50c1t0eXBlXS5sZW5ndGgpO1xuICAgICAgaWYgKHR5cGVvZiBjb25zb2xlLnRyYWNlID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgIC8vIG5vdCBzdXBwb3J0ZWQgaW4gSUUgMTBcbiAgICAgICAgY29uc29sZS50cmFjZSgpO1xuICAgICAgfVxuICAgIH1cbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5vbiA9IEV2ZW50RW1pdHRlci5wcm90b3R5cGUuYWRkTGlzdGVuZXI7XG5cbkV2ZW50RW1pdHRlci5wcm90b3R5cGUub25jZSA9IGZ1bmN0aW9uKHR5cGUsIGxpc3RlbmVyKSB7XG4gIGlmICghaXNGdW5jdGlvbihsaXN0ZW5lcikpXG4gICAgdGhyb3cgVHlwZUVycm9yKCdsaXN0ZW5lciBtdXN0IGJlIGEgZnVuY3Rpb24nKTtcblxuICB2YXIgZmlyZWQgPSBmYWxzZTtcblxuICBmdW5jdGlvbiBnKCkge1xuICAgIHRoaXMucmVtb3ZlTGlzdGVuZXIodHlwZSwgZyk7XG5cbiAgICBpZiAoIWZpcmVkKSB7XG4gICAgICBmaXJlZCA9IHRydWU7XG4gICAgICBsaXN0ZW5lci5hcHBseSh0aGlzLCBhcmd1bWVudHMpO1xuICAgIH1cbiAgfVxuXG4gIGcubGlzdGVuZXIgPSBsaXN0ZW5lcjtcbiAgdGhpcy5vbih0eXBlLCBnKTtcblxuICByZXR1cm4gdGhpcztcbn07XG5cbi8vIGVtaXRzIGEgJ3JlbW92ZUxpc3RlbmVyJyBldmVudCBpZmYgdGhlIGxpc3RlbmVyIHdhcyByZW1vdmVkXG5FdmVudEVtaXR0ZXIucHJvdG90eXBlLnJlbW92ZUxpc3RlbmVyID0gZnVuY3Rpb24odHlwZSwgbGlzdGVuZXIpIHtcbiAgdmFyIGxpc3QsIHBvc2l0aW9uLCBsZW5ndGgsIGk7XG5cbiAgaWYgKCFpc0Z1bmN0aW9uKGxpc3RlbmVyKSlcbiAgICB0aHJvdyBUeXBlRXJyb3IoJ2xpc3RlbmVyIG11c3QgYmUgYSBmdW5jdGlvbicpO1xuXG4gIGlmICghdGhpcy5fZXZlbnRzIHx8ICF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgcmV0dXJuIHRoaXM7XG5cbiAgbGlzdCA9IHRoaXMuX2V2ZW50c1t0eXBlXTtcbiAgbGVuZ3RoID0gbGlzdC5sZW5ndGg7XG4gIHBvc2l0aW9uID0gLTE7XG5cbiAgaWYgKGxpc3QgPT09IGxpc3RlbmVyIHx8XG4gICAgICAoaXNGdW5jdGlvbihsaXN0Lmxpc3RlbmVyKSAmJiBsaXN0Lmxpc3RlbmVyID09PSBsaXN0ZW5lcikpIHtcbiAgICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuICAgIGlmICh0aGlzLl9ldmVudHMucmVtb3ZlTGlzdGVuZXIpXG4gICAgICB0aGlzLmVtaXQoJ3JlbW92ZUxpc3RlbmVyJywgdHlwZSwgbGlzdGVuZXIpO1xuXG4gIH0gZWxzZSBpZiAoaXNPYmplY3QobGlzdCkpIHtcbiAgICBmb3IgKGkgPSBsZW5ndGg7IGktLSA+IDA7KSB7XG4gICAgICBpZiAobGlzdFtpXSA9PT0gbGlzdGVuZXIgfHxcbiAgICAgICAgICAobGlzdFtpXS5saXN0ZW5lciAmJiBsaXN0W2ldLmxpc3RlbmVyID09PSBsaXN0ZW5lcikpIHtcbiAgICAgICAgcG9zaXRpb24gPSBpO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cbiAgICB9XG5cbiAgICBpZiAocG9zaXRpb24gPCAwKVxuICAgICAgcmV0dXJuIHRoaXM7XG5cbiAgICBpZiAobGlzdC5sZW5ndGggPT09IDEpIHtcbiAgICAgIGxpc3QubGVuZ3RoID0gMDtcbiAgICAgIGRlbGV0ZSB0aGlzLl9ldmVudHNbdHlwZV07XG4gICAgfSBlbHNlIHtcbiAgICAgIGxpc3Quc3BsaWNlKHBvc2l0aW9uLCAxKTtcbiAgICB9XG5cbiAgICBpZiAodGhpcy5fZXZlbnRzLnJlbW92ZUxpc3RlbmVyKVxuICAgICAgdGhpcy5lbWl0KCdyZW1vdmVMaXN0ZW5lcicsIHR5cGUsIGxpc3RlbmVyKTtcbiAgfVxuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5yZW1vdmVBbGxMaXN0ZW5lcnMgPSBmdW5jdGlvbih0eXBlKSB7XG4gIHZhciBrZXksIGxpc3RlbmVycztcblxuICBpZiAoIXRoaXMuX2V2ZW50cylcbiAgICByZXR1cm4gdGhpcztcblxuICAvLyBub3QgbGlzdGVuaW5nIGZvciByZW1vdmVMaXN0ZW5lciwgbm8gbmVlZCB0byBlbWl0XG4gIGlmICghdGhpcy5fZXZlbnRzLnJlbW92ZUxpc3RlbmVyKSB7XG4gICAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPT09IDApXG4gICAgICB0aGlzLl9ldmVudHMgPSB7fTtcbiAgICBlbHNlIGlmICh0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgLy8gZW1pdCByZW1vdmVMaXN0ZW5lciBmb3IgYWxsIGxpc3RlbmVycyBvbiBhbGwgZXZlbnRzXG4gIGlmIChhcmd1bWVudHMubGVuZ3RoID09PSAwKSB7XG4gICAgZm9yIChrZXkgaW4gdGhpcy5fZXZlbnRzKSB7XG4gICAgICBpZiAoa2V5ID09PSAncmVtb3ZlTGlzdGVuZXInKSBjb250aW51ZTtcbiAgICAgIHRoaXMucmVtb3ZlQWxsTGlzdGVuZXJzKGtleSk7XG4gICAgfVxuICAgIHRoaXMucmVtb3ZlQWxsTGlzdGVuZXJzKCdyZW1vdmVMaXN0ZW5lcicpO1xuICAgIHRoaXMuX2V2ZW50cyA9IHt9O1xuICAgIHJldHVybiB0aGlzO1xuICB9XG5cbiAgbGlzdGVuZXJzID0gdGhpcy5fZXZlbnRzW3R5cGVdO1xuXG4gIGlmIChpc0Z1bmN0aW9uKGxpc3RlbmVycykpIHtcbiAgICB0aGlzLnJlbW92ZUxpc3RlbmVyKHR5cGUsIGxpc3RlbmVycyk7XG4gIH0gZWxzZSBpZiAobGlzdGVuZXJzKSB7XG4gICAgLy8gTElGTyBvcmRlclxuICAgIHdoaWxlIChsaXN0ZW5lcnMubGVuZ3RoKVxuICAgICAgdGhpcy5yZW1vdmVMaXN0ZW5lcih0eXBlLCBsaXN0ZW5lcnNbbGlzdGVuZXJzLmxlbmd0aCAtIDFdKTtcbiAgfVxuICBkZWxldGUgdGhpcy5fZXZlbnRzW3R5cGVdO1xuXG4gIHJldHVybiB0aGlzO1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5saXN0ZW5lcnMgPSBmdW5jdGlvbih0eXBlKSB7XG4gIHZhciByZXQ7XG4gIGlmICghdGhpcy5fZXZlbnRzIHx8ICF0aGlzLl9ldmVudHNbdHlwZV0pXG4gICAgcmV0ID0gW107XG4gIGVsc2UgaWYgKGlzRnVuY3Rpb24odGhpcy5fZXZlbnRzW3R5cGVdKSlcbiAgICByZXQgPSBbdGhpcy5fZXZlbnRzW3R5cGVdXTtcbiAgZWxzZVxuICAgIHJldCA9IHRoaXMuX2V2ZW50c1t0eXBlXS5zbGljZSgpO1xuICByZXR1cm4gcmV0O1xufTtcblxuRXZlbnRFbWl0dGVyLnByb3RvdHlwZS5saXN0ZW5lckNvdW50ID0gZnVuY3Rpb24odHlwZSkge1xuICBpZiAodGhpcy5fZXZlbnRzKSB7XG4gICAgdmFyIGV2bGlzdGVuZXIgPSB0aGlzLl9ldmVudHNbdHlwZV07XG5cbiAgICBpZiAoaXNGdW5jdGlvbihldmxpc3RlbmVyKSlcbiAgICAgIHJldHVybiAxO1xuICAgIGVsc2UgaWYgKGV2bGlzdGVuZXIpXG4gICAgICByZXR1cm4gZXZsaXN0ZW5lci5sZW5ndGg7XG4gIH1cbiAgcmV0dXJuIDA7XG59O1xuXG5FdmVudEVtaXR0ZXIubGlzdGVuZXJDb3VudCA9IGZ1bmN0aW9uKGVtaXR0ZXIsIHR5cGUpIHtcbiAgcmV0dXJuIGVtaXR0ZXIubGlzdGVuZXJDb3VudCh0eXBlKTtcbn07XG5cbmZ1bmN0aW9uIGlzRnVuY3Rpb24oYXJnKSB7XG4gIHJldHVybiB0eXBlb2YgYXJnID09PSAnZnVuY3Rpb24nO1xufVxuXG5mdW5jdGlvbiBpc051bWJlcihhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdudW1iZXInO1xufVxuXG5mdW5jdGlvbiBpc09iamVjdChhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdvYmplY3QnICYmIGFyZyAhPT0gbnVsbDtcbn1cblxuZnVuY3Rpb24gaXNVbmRlZmluZWQoYXJnKSB7XG4gIHJldHVybiBhcmcgPT09IHZvaWQgMDtcbn1cbiIsIi8vIHNoaW0gZm9yIHVzaW5nIHByb2Nlc3MgaW4gYnJvd3NlclxudmFyIHByb2Nlc3MgPSBtb2R1bGUuZXhwb3J0cyA9IHt9O1xuXG4vLyBjYWNoZWQgZnJvbSB3aGF0ZXZlciBnbG9iYWwgaXMgcHJlc2VudCBzbyB0aGF0IHRlc3QgcnVubmVycyB0aGF0IHN0dWIgaXRcbi8vIGRvbid0IGJyZWFrIHRoaW5ncy4gIEJ1dCB3ZSBuZWVkIHRvIHdyYXAgaXQgaW4gYSB0cnkgY2F0Y2ggaW4gY2FzZSBpdCBpc1xuLy8gd3JhcHBlZCBpbiBzdHJpY3QgbW9kZSBjb2RlIHdoaWNoIGRvZXNuJ3QgZGVmaW5lIGFueSBnbG9iYWxzLiAgSXQncyBpbnNpZGUgYVxuLy8gZnVuY3Rpb24gYmVjYXVzZSB0cnkvY2F0Y2hlcyBkZW9wdGltaXplIGluIGNlcnRhaW4gZW5naW5lcy5cblxudmFyIGNhY2hlZFNldFRpbWVvdXQ7XG52YXIgY2FjaGVkQ2xlYXJUaW1lb3V0O1xuXG5mdW5jdGlvbiBkZWZhdWx0U2V0VGltb3V0KCkge1xuICAgIHRocm93IG5ldyBFcnJvcignc2V0VGltZW91dCBoYXMgbm90IGJlZW4gZGVmaW5lZCcpO1xufVxuZnVuY3Rpb24gZGVmYXVsdENsZWFyVGltZW91dCAoKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdjbGVhclRpbWVvdXQgaGFzIG5vdCBiZWVuIGRlZmluZWQnKTtcbn1cbihmdW5jdGlvbiAoKSB7XG4gICAgdHJ5IHtcbiAgICAgICAgaWYgKHR5cGVvZiBzZXRUaW1lb3V0ID09PSAnZnVuY3Rpb24nKSB7XG4gICAgICAgICAgICBjYWNoZWRTZXRUaW1lb3V0ID0gc2V0VGltZW91dDtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBkZWZhdWx0U2V0VGltb3V0O1xuICAgICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgICBjYWNoZWRTZXRUaW1lb3V0ID0gZGVmYXVsdFNldFRpbW91dDtcbiAgICB9XG4gICAgdHJ5IHtcbiAgICAgICAgaWYgKHR5cGVvZiBjbGVhclRpbWVvdXQgPT09ICdmdW5jdGlvbicpIHtcbiAgICAgICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGNsZWFyVGltZW91dDtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGRlZmF1bHRDbGVhclRpbWVvdXQ7XG4gICAgICAgIH1cbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICAgIGNhY2hlZENsZWFyVGltZW91dCA9IGRlZmF1bHRDbGVhclRpbWVvdXQ7XG4gICAgfVxufSAoKSlcbmZ1bmN0aW9uIHJ1blRpbWVvdXQoZnVuKSB7XG4gICAgaWYgKGNhY2hlZFNldFRpbWVvdXQgPT09IHNldFRpbWVvdXQpIHtcbiAgICAgICAgLy9ub3JtYWwgZW52aXJvbWVudHMgaW4gc2FuZSBzaXR1YXRpb25zXG4gICAgICAgIHJldHVybiBzZXRUaW1lb3V0KGZ1biwgMCk7XG4gICAgfVxuICAgIC8vIGlmIHNldFRpbWVvdXQgd2Fzbid0IGF2YWlsYWJsZSBidXQgd2FzIGxhdHRlciBkZWZpbmVkXG4gICAgaWYgKChjYWNoZWRTZXRUaW1lb3V0ID09PSBkZWZhdWx0U2V0VGltb3V0IHx8ICFjYWNoZWRTZXRUaW1lb3V0KSAmJiBzZXRUaW1lb3V0KSB7XG4gICAgICAgIGNhY2hlZFNldFRpbWVvdXQgPSBzZXRUaW1lb3V0O1xuICAgICAgICByZXR1cm4gc2V0VGltZW91dChmdW4sIDApO1xuICAgIH1cbiAgICB0cnkge1xuICAgICAgICAvLyB3aGVuIHdoZW4gc29tZWJvZHkgaGFzIHNjcmV3ZWQgd2l0aCBzZXRUaW1lb3V0IGJ1dCBubyBJLkUuIG1hZGRuZXNzXG4gICAgICAgIHJldHVybiBjYWNoZWRTZXRUaW1lb3V0KGZ1biwgMCk7XG4gICAgfSBjYXRjaChlKXtcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICAgIC8vIFdoZW4gd2UgYXJlIGluIEkuRS4gYnV0IHRoZSBzY3JpcHQgaGFzIGJlZW4gZXZhbGVkIHNvIEkuRS4gZG9lc24ndCB0cnVzdCB0aGUgZ2xvYmFsIG9iamVjdCB3aGVuIGNhbGxlZCBub3JtYWxseVxuICAgICAgICAgICAgcmV0dXJuIGNhY2hlZFNldFRpbWVvdXQuY2FsbChudWxsLCBmdW4sIDApO1xuICAgICAgICB9IGNhdGNoKGUpe1xuICAgICAgICAgICAgLy8gc2FtZSBhcyBhYm92ZSBidXQgd2hlbiBpdCdzIGEgdmVyc2lvbiBvZiBJLkUuIHRoYXQgbXVzdCBoYXZlIHRoZSBnbG9iYWwgb2JqZWN0IGZvciAndGhpcycsIGhvcGZ1bGx5IG91ciBjb250ZXh0IGNvcnJlY3Qgb3RoZXJ3aXNlIGl0IHdpbGwgdGhyb3cgYSBnbG9iYWwgZXJyb3JcbiAgICAgICAgICAgIHJldHVybiBjYWNoZWRTZXRUaW1lb3V0LmNhbGwodGhpcywgZnVuLCAwKTtcbiAgICAgICAgfVxuICAgIH1cblxuXG59XG5mdW5jdGlvbiBydW5DbGVhclRpbWVvdXQobWFya2VyKSB7XG4gICAgaWYgKGNhY2hlZENsZWFyVGltZW91dCA9PT0gY2xlYXJUaW1lb3V0KSB7XG4gICAgICAgIC8vbm9ybWFsIGVudmlyb21lbnRzIGluIHNhbmUgc2l0dWF0aW9uc1xuICAgICAgICByZXR1cm4gY2xlYXJUaW1lb3V0KG1hcmtlcik7XG4gICAgfVxuICAgIC8vIGlmIGNsZWFyVGltZW91dCB3YXNuJ3QgYXZhaWxhYmxlIGJ1dCB3YXMgbGF0dGVyIGRlZmluZWRcbiAgICBpZiAoKGNhY2hlZENsZWFyVGltZW91dCA9PT0gZGVmYXVsdENsZWFyVGltZW91dCB8fCAhY2FjaGVkQ2xlYXJUaW1lb3V0KSAmJiBjbGVhclRpbWVvdXQpIHtcbiAgICAgICAgY2FjaGVkQ2xlYXJUaW1lb3V0ID0gY2xlYXJUaW1lb3V0O1xuICAgICAgICByZXR1cm4gY2xlYXJUaW1lb3V0KG1hcmtlcik7XG4gICAgfVxuICAgIHRyeSB7XG4gICAgICAgIC8vIHdoZW4gd2hlbiBzb21lYm9keSBoYXMgc2NyZXdlZCB3aXRoIHNldFRpbWVvdXQgYnV0IG5vIEkuRS4gbWFkZG5lc3NcbiAgICAgICAgcmV0dXJuIGNhY2hlZENsZWFyVGltZW91dChtYXJrZXIpO1xuICAgIH0gY2F0Y2ggKGUpe1xuICAgICAgICB0cnkge1xuICAgICAgICAgICAgLy8gV2hlbiB3ZSBhcmUgaW4gSS5FLiBidXQgdGhlIHNjcmlwdCBoYXMgYmVlbiBldmFsZWQgc28gSS5FLiBkb2Vzbid0ICB0cnVzdCB0aGUgZ2xvYmFsIG9iamVjdCB3aGVuIGNhbGxlZCBub3JtYWxseVxuICAgICAgICAgICAgcmV0dXJuIGNhY2hlZENsZWFyVGltZW91dC5jYWxsKG51bGwsIG1hcmtlcik7XG4gICAgICAgIH0gY2F0Y2ggKGUpe1xuICAgICAgICAgICAgLy8gc2FtZSBhcyBhYm92ZSBidXQgd2hlbiBpdCdzIGEgdmVyc2lvbiBvZiBJLkUuIHRoYXQgbXVzdCBoYXZlIHRoZSBnbG9iYWwgb2JqZWN0IGZvciAndGhpcycsIGhvcGZ1bGx5IG91ciBjb250ZXh0IGNvcnJlY3Qgb3RoZXJ3aXNlIGl0IHdpbGwgdGhyb3cgYSBnbG9iYWwgZXJyb3IuXG4gICAgICAgICAgICAvLyBTb21lIHZlcnNpb25zIG9mIEkuRS4gaGF2ZSBkaWZmZXJlbnQgcnVsZXMgZm9yIGNsZWFyVGltZW91dCB2cyBzZXRUaW1lb3V0XG4gICAgICAgICAgICByZXR1cm4gY2FjaGVkQ2xlYXJUaW1lb3V0LmNhbGwodGhpcywgbWFya2VyKTtcbiAgICAgICAgfVxuICAgIH1cblxuXG5cbn1cbnZhciBxdWV1ZSA9IFtdO1xudmFyIGRyYWluaW5nID0gZmFsc2U7XG52YXIgY3VycmVudFF1ZXVlO1xudmFyIHF1ZXVlSW5kZXggPSAtMTtcblxuZnVuY3Rpb24gY2xlYW5VcE5leHRUaWNrKCkge1xuICAgIGlmICghZHJhaW5pbmcgfHwgIWN1cnJlbnRRdWV1ZSkge1xuICAgICAgICByZXR1cm47XG4gICAgfVxuICAgIGRyYWluaW5nID0gZmFsc2U7XG4gICAgaWYgKGN1cnJlbnRRdWV1ZS5sZW5ndGgpIHtcbiAgICAgICAgcXVldWUgPSBjdXJyZW50UXVldWUuY29uY2F0KHF1ZXVlKTtcbiAgICB9IGVsc2Uge1xuICAgICAgICBxdWV1ZUluZGV4ID0gLTE7XG4gICAgfVxuICAgIGlmIChxdWV1ZS5sZW5ndGgpIHtcbiAgICAgICAgZHJhaW5RdWV1ZSgpO1xuICAgIH1cbn1cblxuZnVuY3Rpb24gZHJhaW5RdWV1ZSgpIHtcbiAgICBpZiAoZHJhaW5pbmcpIHtcbiAgICAgICAgcmV0dXJuO1xuICAgIH1cbiAgICB2YXIgdGltZW91dCA9IHJ1blRpbWVvdXQoY2xlYW5VcE5leHRUaWNrKTtcbiAgICBkcmFpbmluZyA9IHRydWU7XG5cbiAgICB2YXIgbGVuID0gcXVldWUubGVuZ3RoO1xuICAgIHdoaWxlKGxlbikge1xuICAgICAgICBjdXJyZW50UXVldWUgPSBxdWV1ZTtcbiAgICAgICAgcXVldWUgPSBbXTtcbiAgICAgICAgd2hpbGUgKCsrcXVldWVJbmRleCA8IGxlbikge1xuICAgICAgICAgICAgaWYgKGN1cnJlbnRRdWV1ZSkge1xuICAgICAgICAgICAgICAgIGN1cnJlbnRRdWV1ZVtxdWV1ZUluZGV4XS5ydW4oKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICBxdWV1ZUluZGV4ID0gLTE7XG4gICAgICAgIGxlbiA9IHF1ZXVlLmxlbmd0aDtcbiAgICB9XG4gICAgY3VycmVudFF1ZXVlID0gbnVsbDtcbiAgICBkcmFpbmluZyA9IGZhbHNlO1xuICAgIHJ1bkNsZWFyVGltZW91dCh0aW1lb3V0KTtcbn1cblxucHJvY2Vzcy5uZXh0VGljayA9IGZ1bmN0aW9uIChmdW4pIHtcbiAgICB2YXIgYXJncyA9IG5ldyBBcnJheShhcmd1bWVudHMubGVuZ3RoIC0gMSk7XG4gICAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPiAxKSB7XG4gICAgICAgIGZvciAodmFyIGkgPSAxOyBpIDwgYXJndW1lbnRzLmxlbmd0aDsgaSsrKSB7XG4gICAgICAgICAgICBhcmdzW2kgLSAxXSA9IGFyZ3VtZW50c1tpXTtcbiAgICAgICAgfVxuICAgIH1cbiAgICBxdWV1ZS5wdXNoKG5ldyBJdGVtKGZ1biwgYXJncykpO1xuICAgIGlmIChxdWV1ZS5sZW5ndGggPT09IDEgJiYgIWRyYWluaW5nKSB7XG4gICAgICAgIHJ1blRpbWVvdXQoZHJhaW5RdWV1ZSk7XG4gICAgfVxufTtcblxuLy8gdjggbGlrZXMgcHJlZGljdGlibGUgb2JqZWN0c1xuZnVuY3Rpb24gSXRlbShmdW4sIGFycmF5KSB7XG4gICAgdGhpcy5mdW4gPSBmdW47XG4gICAgdGhpcy5hcnJheSA9IGFycmF5O1xufVxuSXRlbS5wcm90b3R5cGUucnVuID0gZnVuY3Rpb24gKCkge1xuICAgIHRoaXMuZnVuLmFwcGx5KG51bGwsIHRoaXMuYXJyYXkpO1xufTtcbnByb2Nlc3MudGl0bGUgPSAnYnJvd3Nlcic7XG5wcm9jZXNzLmJyb3dzZXIgPSB0cnVlO1xucHJvY2Vzcy5lbnYgPSB7fTtcbnByb2Nlc3MuYXJndiA9IFtdO1xucHJvY2Vzcy52ZXJzaW9uID0gJyc7IC8vIGVtcHR5IHN0cmluZyB0byBhdm9pZCByZWdleHAgaXNzdWVzXG5wcm9jZXNzLnZlcnNpb25zID0ge307XG5cbmZ1bmN0aW9uIG5vb3AoKSB7fVxuXG5wcm9jZXNzLm9uID0gbm9vcDtcbnByb2Nlc3MuYWRkTGlzdGVuZXIgPSBub29wO1xucHJvY2Vzcy5vbmNlID0gbm9vcDtcbnByb2Nlc3Mub2ZmID0gbm9vcDtcbnByb2Nlc3MucmVtb3ZlTGlzdGVuZXIgPSBub29wO1xucHJvY2Vzcy5yZW1vdmVBbGxMaXN0ZW5lcnMgPSBub29wO1xucHJvY2Vzcy5lbWl0ID0gbm9vcDtcblxucHJvY2Vzcy5iaW5kaW5nID0gZnVuY3Rpb24gKG5hbWUpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ3Byb2Nlc3MuYmluZGluZyBpcyBub3Qgc3VwcG9ydGVkJyk7XG59O1xuXG5wcm9jZXNzLmN3ZCA9IGZ1bmN0aW9uICgpIHsgcmV0dXJuICcvJyB9O1xucHJvY2Vzcy5jaGRpciA9IGZ1bmN0aW9uIChkaXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ3Byb2Nlc3MuY2hkaXIgaXMgbm90IHN1cHBvcnRlZCcpO1xufTtcbnByb2Nlc3MudW1hc2sgPSBmdW5jdGlvbigpIHsgcmV0dXJuIDA7IH07XG4iLCJpZiAodHlwZW9mIE9iamVjdC5jcmVhdGUgPT09ICdmdW5jdGlvbicpIHtcbiAgLy8gaW1wbGVtZW50YXRpb24gZnJvbSBzdGFuZGFyZCBub2RlLmpzICd1dGlsJyBtb2R1bGVcbiAgbW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiBpbmhlcml0cyhjdG9yLCBzdXBlckN0b3IpIHtcbiAgICBjdG9yLnN1cGVyXyA9IHN1cGVyQ3RvclxuICAgIGN0b3IucHJvdG90eXBlID0gT2JqZWN0LmNyZWF0ZShzdXBlckN0b3IucHJvdG90eXBlLCB7XG4gICAgICBjb25zdHJ1Y3Rvcjoge1xuICAgICAgICB2YWx1ZTogY3RvcixcbiAgICAgICAgZW51bWVyYWJsZTogZmFsc2UsXG4gICAgICAgIHdyaXRhYmxlOiB0cnVlLFxuICAgICAgICBjb25maWd1cmFibGU6IHRydWVcbiAgICAgIH1cbiAgICB9KTtcbiAgfTtcbn0gZWxzZSB7XG4gIC8vIG9sZCBzY2hvb2wgc2hpbSBmb3Igb2xkIGJyb3dzZXJzXG4gIG1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaW5oZXJpdHMoY3Rvciwgc3VwZXJDdG9yKSB7XG4gICAgY3Rvci5zdXBlcl8gPSBzdXBlckN0b3JcbiAgICB2YXIgVGVtcEN0b3IgPSBmdW5jdGlvbiAoKSB7fVxuICAgIFRlbXBDdG9yLnByb3RvdHlwZSA9IHN1cGVyQ3Rvci5wcm90b3R5cGVcbiAgICBjdG9yLnByb3RvdHlwZSA9IG5ldyBUZW1wQ3RvcigpXG4gICAgY3Rvci5wcm90b3R5cGUuY29uc3RydWN0b3IgPSBjdG9yXG4gIH1cbn1cbiIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24gaXNCdWZmZXIoYXJnKSB7XG4gIHJldHVybiBhcmcgJiYgdHlwZW9mIGFyZyA9PT0gJ29iamVjdCdcbiAgICAmJiB0eXBlb2YgYXJnLmNvcHkgPT09ICdmdW5jdGlvbidcbiAgICAmJiB0eXBlb2YgYXJnLmZpbGwgPT09ICdmdW5jdGlvbidcbiAgICAmJiB0eXBlb2YgYXJnLnJlYWRVSW50OCA9PT0gJ2Z1bmN0aW9uJztcbn0iLCIvLyBDb3B5cmlnaHQgSm95ZW50LCBJbmMuIGFuZCBvdGhlciBOb2RlIGNvbnRyaWJ1dG9ycy5cbi8vXG4vLyBQZXJtaXNzaW9uIGlzIGhlcmVieSBncmFudGVkLCBmcmVlIG9mIGNoYXJnZSwgdG8gYW55IHBlcnNvbiBvYnRhaW5pbmcgYVxuLy8gY29weSBvZiB0aGlzIHNvZnR3YXJlIGFuZCBhc3NvY2lhdGVkIGRvY3VtZW50YXRpb24gZmlsZXMgKHRoZVxuLy8gXCJTb2Z0d2FyZVwiKSwgdG8gZGVhbCBpbiB0aGUgU29mdHdhcmUgd2l0aG91dCByZXN0cmljdGlvbiwgaW5jbHVkaW5nXG4vLyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cyB0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsXG4vLyBkaXN0cmlidXRlLCBzdWJsaWNlbnNlLCBhbmQvb3Igc2VsbCBjb3BpZXMgb2YgdGhlIFNvZnR3YXJlLCBhbmQgdG8gcGVybWl0XG4vLyBwZXJzb25zIHRvIHdob20gdGhlIFNvZnR3YXJlIGlzIGZ1cm5pc2hlZCB0byBkbyBzbywgc3ViamVjdCB0byB0aGVcbi8vIGZvbGxvd2luZyBjb25kaXRpb25zOlxuLy9cbi8vIFRoZSBhYm92ZSBjb3B5cmlnaHQgbm90aWNlIGFuZCB0aGlzIHBlcm1pc3Npb24gbm90aWNlIHNoYWxsIGJlIGluY2x1ZGVkXG4vLyBpbiBhbGwgY29waWVzIG9yIHN1YnN0YW50aWFsIHBvcnRpb25zIG9mIHRoZSBTb2Z0d2FyZS5cbi8vXG4vLyBUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgXCJBUyBJU1wiLCBXSVRIT1VUIFdBUlJBTlRZIE9GIEFOWSBLSU5ELCBFWFBSRVNTXG4vLyBPUiBJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GXG4vLyBNRVJDSEFOVEFCSUxJVFksIEZJVE5FU1MgRk9SIEEgUEFSVElDVUxBUiBQVVJQT1NFIEFORCBOT05JTkZSSU5HRU1FTlQuIElOXG4vLyBOTyBFVkVOVCBTSEFMTCBUSEUgQVVUSE9SUyBPUiBDT1BZUklHSFQgSE9MREVSUyBCRSBMSUFCTEUgRk9SIEFOWSBDTEFJTSxcbi8vIERBTUFHRVMgT1IgT1RIRVIgTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUlxuLy8gT1RIRVJXSVNFLCBBUklTSU5HIEZST00sIE9VVCBPRiBPUiBJTiBDT05ORUNUSU9OIFdJVEggVEhFIFNPRlRXQVJFIE9SIFRIRVxuLy8gVVNFIE9SIE9USEVSIERFQUxJTkdTIElOIFRIRSBTT0ZUV0FSRS5cblxudmFyIGZvcm1hdFJlZ0V4cCA9IC8lW3NkaiVdL2c7XG5leHBvcnRzLmZvcm1hdCA9IGZ1bmN0aW9uKGYpIHtcbiAgaWYgKCFpc1N0cmluZyhmKSkge1xuICAgIHZhciBvYmplY3RzID0gW107XG4gICAgZm9yICh2YXIgaSA9IDA7IGkgPCBhcmd1bWVudHMubGVuZ3RoOyBpKyspIHtcbiAgICAgIG9iamVjdHMucHVzaChpbnNwZWN0KGFyZ3VtZW50c1tpXSkpO1xuICAgIH1cbiAgICByZXR1cm4gb2JqZWN0cy5qb2luKCcgJyk7XG4gIH1cblxuICB2YXIgaSA9IDE7XG4gIHZhciBhcmdzID0gYXJndW1lbnRzO1xuICB2YXIgbGVuID0gYXJncy5sZW5ndGg7XG4gIHZhciBzdHIgPSBTdHJpbmcoZikucmVwbGFjZShmb3JtYXRSZWdFeHAsIGZ1bmN0aW9uKHgpIHtcbiAgICBpZiAoeCA9PT0gJyUlJykgcmV0dXJuICclJztcbiAgICBpZiAoaSA+PSBsZW4pIHJldHVybiB4O1xuICAgIHN3aXRjaCAoeCkge1xuICAgICAgY2FzZSAnJXMnOiByZXR1cm4gU3RyaW5nKGFyZ3NbaSsrXSk7XG4gICAgICBjYXNlICclZCc6IHJldHVybiBOdW1iZXIoYXJnc1tpKytdKTtcbiAgICAgIGNhc2UgJyVqJzpcbiAgICAgICAgdHJ5IHtcbiAgICAgICAgICByZXR1cm4gSlNPTi5zdHJpbmdpZnkoYXJnc1tpKytdKTtcbiAgICAgICAgfSBjYXRjaCAoXykge1xuICAgICAgICAgIHJldHVybiAnW0NpcmN1bGFyXSc7XG4gICAgICAgIH1cbiAgICAgIGRlZmF1bHQ6XG4gICAgICAgIHJldHVybiB4O1xuICAgIH1cbiAgfSk7XG4gIGZvciAodmFyIHggPSBhcmdzW2ldOyBpIDwgbGVuOyB4ID0gYXJnc1srK2ldKSB7XG4gICAgaWYgKGlzTnVsbCh4KSB8fCAhaXNPYmplY3QoeCkpIHtcbiAgICAgIHN0ciArPSAnICcgKyB4O1xuICAgIH0gZWxzZSB7XG4gICAgICBzdHIgKz0gJyAnICsgaW5zcGVjdCh4KTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHN0cjtcbn07XG5cblxuLy8gTWFyayB0aGF0IGEgbWV0aG9kIHNob3VsZCBub3QgYmUgdXNlZC5cbi8vIFJldHVybnMgYSBtb2RpZmllZCBmdW5jdGlvbiB3aGljaCB3YXJucyBvbmNlIGJ5IGRlZmF1bHQuXG4vLyBJZiAtLW5vLWRlcHJlY2F0aW9uIGlzIHNldCwgdGhlbiBpdCBpcyBhIG5vLW9wLlxuZXhwb3J0cy5kZXByZWNhdGUgPSBmdW5jdGlvbihmbiwgbXNnKSB7XG4gIC8vIEFsbG93IGZvciBkZXByZWNhdGluZyB0aGluZ3MgaW4gdGhlIHByb2Nlc3Mgb2Ygc3RhcnRpbmcgdXAuXG4gIGlmIChpc1VuZGVmaW5lZChnbG9iYWwucHJvY2VzcykpIHtcbiAgICByZXR1cm4gZnVuY3Rpb24oKSB7XG4gICAgICByZXR1cm4gZXhwb3J0cy5kZXByZWNhdGUoZm4sIG1zZykuYXBwbHkodGhpcywgYXJndW1lbnRzKTtcbiAgICB9O1xuICB9XG5cbiAgaWYgKHByb2Nlc3Mubm9EZXByZWNhdGlvbiA9PT0gdHJ1ZSkge1xuICAgIHJldHVybiBmbjtcbiAgfVxuXG4gIHZhciB3YXJuZWQgPSBmYWxzZTtcbiAgZnVuY3Rpb24gZGVwcmVjYXRlZCgpIHtcbiAgICBpZiAoIXdhcm5lZCkge1xuICAgICAgaWYgKHByb2Nlc3MudGhyb3dEZXByZWNhdGlvbikge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IobXNnKTtcbiAgICAgIH0gZWxzZSBpZiAocHJvY2Vzcy50cmFjZURlcHJlY2F0aW9uKSB7XG4gICAgICAgIGNvbnNvbGUudHJhY2UobXNnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGNvbnNvbGUuZXJyb3IobXNnKTtcbiAgICAgIH1cbiAgICAgIHdhcm5lZCA9IHRydWU7XG4gICAgfVxuICAgIHJldHVybiBmbi5hcHBseSh0aGlzLCBhcmd1bWVudHMpO1xuICB9XG5cbiAgcmV0dXJuIGRlcHJlY2F0ZWQ7XG59O1xuXG5cbnZhciBkZWJ1Z3MgPSB7fTtcbnZhciBkZWJ1Z0Vudmlyb247XG5leHBvcnRzLmRlYnVnbG9nID0gZnVuY3Rpb24oc2V0KSB7XG4gIGlmIChpc1VuZGVmaW5lZChkZWJ1Z0Vudmlyb24pKVxuICAgIGRlYnVnRW52aXJvbiA9IHByb2Nlc3MuZW52Lk5PREVfREVCVUcgfHwgJyc7XG4gIHNldCA9IHNldC50b1VwcGVyQ2FzZSgpO1xuICBpZiAoIWRlYnVnc1tzZXRdKSB7XG4gICAgaWYgKG5ldyBSZWdFeHAoJ1xcXFxiJyArIHNldCArICdcXFxcYicsICdpJykudGVzdChkZWJ1Z0Vudmlyb24pKSB7XG4gICAgICB2YXIgcGlkID0gcHJvY2Vzcy5waWQ7XG4gICAgICBkZWJ1Z3Nbc2V0XSA9IGZ1bmN0aW9uKCkge1xuICAgICAgICB2YXIgbXNnID0gZXhwb3J0cy5mb3JtYXQuYXBwbHkoZXhwb3J0cywgYXJndW1lbnRzKTtcbiAgICAgICAgY29uc29sZS5lcnJvcignJXMgJWQ6ICVzJywgc2V0LCBwaWQsIG1zZyk7XG4gICAgICB9O1xuICAgIH0gZWxzZSB7XG4gICAgICBkZWJ1Z3Nbc2V0XSA9IGZ1bmN0aW9uKCkge307XG4gICAgfVxuICB9XG4gIHJldHVybiBkZWJ1Z3Nbc2V0XTtcbn07XG5cblxuLyoqXG4gKiBFY2hvcyB0aGUgdmFsdWUgb2YgYSB2YWx1ZS4gVHJ5cyB0byBwcmludCB0aGUgdmFsdWUgb3V0XG4gKiBpbiB0aGUgYmVzdCB3YXkgcG9zc2libGUgZ2l2ZW4gdGhlIGRpZmZlcmVudCB0eXBlcy5cbiAqXG4gKiBAcGFyYW0ge09iamVjdH0gb2JqIFRoZSBvYmplY3QgdG8gcHJpbnQgb3V0LlxuICogQHBhcmFtIHtPYmplY3R9IG9wdHMgT3B0aW9uYWwgb3B0aW9ucyBvYmplY3QgdGhhdCBhbHRlcnMgdGhlIG91dHB1dC5cbiAqL1xuLyogbGVnYWN5OiBvYmosIHNob3dIaWRkZW4sIGRlcHRoLCBjb2xvcnMqL1xuZnVuY3Rpb24gaW5zcGVjdChvYmosIG9wdHMpIHtcbiAgLy8gZGVmYXVsdCBvcHRpb25zXG4gIHZhciBjdHggPSB7XG4gICAgc2VlbjogW10sXG4gICAgc3R5bGl6ZTogc3R5bGl6ZU5vQ29sb3JcbiAgfTtcbiAgLy8gbGVnYWN5Li4uXG4gIGlmIChhcmd1bWVudHMubGVuZ3RoID49IDMpIGN0eC5kZXB0aCA9IGFyZ3VtZW50c1syXTtcbiAgaWYgKGFyZ3VtZW50cy5sZW5ndGggPj0gNCkgY3R4LmNvbG9ycyA9IGFyZ3VtZW50c1szXTtcbiAgaWYgKGlzQm9vbGVhbihvcHRzKSkge1xuICAgIC8vIGxlZ2FjeS4uLlxuICAgIGN0eC5zaG93SGlkZGVuID0gb3B0cztcbiAgfSBlbHNlIGlmIChvcHRzKSB7XG4gICAgLy8gZ290IGFuIFwib3B0aW9uc1wiIG9iamVjdFxuICAgIGV4cG9ydHMuX2V4dGVuZChjdHgsIG9wdHMpO1xuICB9XG4gIC8vIHNldCBkZWZhdWx0IG9wdGlvbnNcbiAgaWYgKGlzVW5kZWZpbmVkKGN0eC5zaG93SGlkZGVuKSkgY3R4LnNob3dIaWRkZW4gPSBmYWxzZTtcbiAgaWYgKGlzVW5kZWZpbmVkKGN0eC5kZXB0aCkpIGN0eC5kZXB0aCA9IDI7XG4gIGlmIChpc1VuZGVmaW5lZChjdHguY29sb3JzKSkgY3R4LmNvbG9ycyA9IGZhbHNlO1xuICBpZiAoaXNVbmRlZmluZWQoY3R4LmN1c3RvbUluc3BlY3QpKSBjdHguY3VzdG9tSW5zcGVjdCA9IHRydWU7XG4gIGlmIChjdHguY29sb3JzKSBjdHguc3R5bGl6ZSA9IHN0eWxpemVXaXRoQ29sb3I7XG4gIHJldHVybiBmb3JtYXRWYWx1ZShjdHgsIG9iaiwgY3R4LmRlcHRoKTtcbn1cbmV4cG9ydHMuaW5zcGVjdCA9IGluc3BlY3Q7XG5cblxuLy8gaHR0cDovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9BTlNJX2VzY2FwZV9jb2RlI2dyYXBoaWNzXG5pbnNwZWN0LmNvbG9ycyA9IHtcbiAgJ2JvbGQnIDogWzEsIDIyXSxcbiAgJ2l0YWxpYycgOiBbMywgMjNdLFxuICAndW5kZXJsaW5lJyA6IFs0LCAyNF0sXG4gICdpbnZlcnNlJyA6IFs3LCAyN10sXG4gICd3aGl0ZScgOiBbMzcsIDM5XSxcbiAgJ2dyZXknIDogWzkwLCAzOV0sXG4gICdibGFjaycgOiBbMzAsIDM5XSxcbiAgJ2JsdWUnIDogWzM0LCAzOV0sXG4gICdjeWFuJyA6IFszNiwgMzldLFxuICAnZ3JlZW4nIDogWzMyLCAzOV0sXG4gICdtYWdlbnRhJyA6IFszNSwgMzldLFxuICAncmVkJyA6IFszMSwgMzldLFxuICAneWVsbG93JyA6IFszMywgMzldXG59O1xuXG4vLyBEb24ndCB1c2UgJ2JsdWUnIG5vdCB2aXNpYmxlIG9uIGNtZC5leGVcbmluc3BlY3Quc3R5bGVzID0ge1xuICAnc3BlY2lhbCc6ICdjeWFuJyxcbiAgJ251bWJlcic6ICd5ZWxsb3cnLFxuICAnYm9vbGVhbic6ICd5ZWxsb3cnLFxuICAndW5kZWZpbmVkJzogJ2dyZXknLFxuICAnbnVsbCc6ICdib2xkJyxcbiAgJ3N0cmluZyc6ICdncmVlbicsXG4gICdkYXRlJzogJ21hZ2VudGEnLFxuICAvLyBcIm5hbWVcIjogaW50ZW50aW9uYWxseSBub3Qgc3R5bGluZ1xuICAncmVnZXhwJzogJ3JlZCdcbn07XG5cblxuZnVuY3Rpb24gc3R5bGl6ZVdpdGhDb2xvcihzdHIsIHN0eWxlVHlwZSkge1xuICB2YXIgc3R5bGUgPSBpbnNwZWN0LnN0eWxlc1tzdHlsZVR5cGVdO1xuXG4gIGlmIChzdHlsZSkge1xuICAgIHJldHVybiAnXFx1MDAxYlsnICsgaW5zcGVjdC5jb2xvcnNbc3R5bGVdWzBdICsgJ20nICsgc3RyICtcbiAgICAgICAgICAgJ1xcdTAwMWJbJyArIGluc3BlY3QuY29sb3JzW3N0eWxlXVsxXSArICdtJztcbiAgfSBlbHNlIHtcbiAgICByZXR1cm4gc3RyO1xuICB9XG59XG5cblxuZnVuY3Rpb24gc3R5bGl6ZU5vQ29sb3Ioc3RyLCBzdHlsZVR5cGUpIHtcbiAgcmV0dXJuIHN0cjtcbn1cblxuXG5mdW5jdGlvbiBhcnJheVRvSGFzaChhcnJheSkge1xuICB2YXIgaGFzaCA9IHt9O1xuXG4gIGFycmF5LmZvckVhY2goZnVuY3Rpb24odmFsLCBpZHgpIHtcbiAgICBoYXNoW3ZhbF0gPSB0cnVlO1xuICB9KTtcblxuICByZXR1cm4gaGFzaDtcbn1cblxuXG5mdW5jdGlvbiBmb3JtYXRWYWx1ZShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMpIHtcbiAgLy8gUHJvdmlkZSBhIGhvb2sgZm9yIHVzZXItc3BlY2lmaWVkIGluc3BlY3QgZnVuY3Rpb25zLlxuICAvLyBDaGVjayB0aGF0IHZhbHVlIGlzIGFuIG9iamVjdCB3aXRoIGFuIGluc3BlY3QgZnVuY3Rpb24gb24gaXRcbiAgaWYgKGN0eC5jdXN0b21JbnNwZWN0ICYmXG4gICAgICB2YWx1ZSAmJlxuICAgICAgaXNGdW5jdGlvbih2YWx1ZS5pbnNwZWN0KSAmJlxuICAgICAgLy8gRmlsdGVyIG91dCB0aGUgdXRpbCBtb2R1bGUsIGl0J3MgaW5zcGVjdCBmdW5jdGlvbiBpcyBzcGVjaWFsXG4gICAgICB2YWx1ZS5pbnNwZWN0ICE9PSBleHBvcnRzLmluc3BlY3QgJiZcbiAgICAgIC8vIEFsc28gZmlsdGVyIG91dCBhbnkgcHJvdG90eXBlIG9iamVjdHMgdXNpbmcgdGhlIGNpcmN1bGFyIGNoZWNrLlxuICAgICAgISh2YWx1ZS5jb25zdHJ1Y3RvciAmJiB2YWx1ZS5jb25zdHJ1Y3Rvci5wcm90b3R5cGUgPT09IHZhbHVlKSkge1xuICAgIHZhciByZXQgPSB2YWx1ZS5pbnNwZWN0KHJlY3Vyc2VUaW1lcywgY3R4KTtcbiAgICBpZiAoIWlzU3RyaW5nKHJldCkpIHtcbiAgICAgIHJldCA9IGZvcm1hdFZhbHVlKGN0eCwgcmV0LCByZWN1cnNlVGltZXMpO1xuICAgIH1cbiAgICByZXR1cm4gcmV0O1xuICB9XG5cbiAgLy8gUHJpbWl0aXZlIHR5cGVzIGNhbm5vdCBoYXZlIHByb3BlcnRpZXNcbiAgdmFyIHByaW1pdGl2ZSA9IGZvcm1hdFByaW1pdGl2ZShjdHgsIHZhbHVlKTtcbiAgaWYgKHByaW1pdGl2ZSkge1xuICAgIHJldHVybiBwcmltaXRpdmU7XG4gIH1cblxuICAvLyBMb29rIHVwIHRoZSBrZXlzIG9mIHRoZSBvYmplY3QuXG4gIHZhciBrZXlzID0gT2JqZWN0LmtleXModmFsdWUpO1xuICB2YXIgdmlzaWJsZUtleXMgPSBhcnJheVRvSGFzaChrZXlzKTtcblxuICBpZiAoY3R4LnNob3dIaWRkZW4pIHtcbiAgICBrZXlzID0gT2JqZWN0LmdldE93blByb3BlcnR5TmFtZXModmFsdWUpO1xuICB9XG5cbiAgLy8gSUUgZG9lc24ndCBtYWtlIGVycm9yIGZpZWxkcyBub24tZW51bWVyYWJsZVxuICAvLyBodHRwOi8vbXNkbi5taWNyb3NvZnQuY29tL2VuLXVzL2xpYnJhcnkvaWUvZHd3NTJzYnQodj12cy45NCkuYXNweFxuICBpZiAoaXNFcnJvcih2YWx1ZSlcbiAgICAgICYmIChrZXlzLmluZGV4T2YoJ21lc3NhZ2UnKSA+PSAwIHx8IGtleXMuaW5kZXhPZignZGVzY3JpcHRpb24nKSA+PSAwKSkge1xuICAgIHJldHVybiBmb3JtYXRFcnJvcih2YWx1ZSk7XG4gIH1cblxuICAvLyBTb21lIHR5cGUgb2Ygb2JqZWN0IHdpdGhvdXQgcHJvcGVydGllcyBjYW4gYmUgc2hvcnRjdXR0ZWQuXG4gIGlmIChrZXlzLmxlbmd0aCA9PT0gMCkge1xuICAgIGlmIChpc0Z1bmN0aW9uKHZhbHVlKSkge1xuICAgICAgdmFyIG5hbWUgPSB2YWx1ZS5uYW1lID8gJzogJyArIHZhbHVlLm5hbWUgOiAnJztcbiAgICAgIHJldHVybiBjdHguc3R5bGl6ZSgnW0Z1bmN0aW9uJyArIG5hbWUgKyAnXScsICdzcGVjaWFsJyk7XG4gICAgfVxuICAgIGlmIChpc1JlZ0V4cCh2YWx1ZSkpIHtcbiAgICAgIHJldHVybiBjdHguc3R5bGl6ZShSZWdFeHAucHJvdG90eXBlLnRvU3RyaW5nLmNhbGwodmFsdWUpLCAncmVnZXhwJyk7XG4gICAgfVxuICAgIGlmIChpc0RhdGUodmFsdWUpKSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoRGF0ZS5wcm90b3R5cGUudG9TdHJpbmcuY2FsbCh2YWx1ZSksICdkYXRlJyk7XG4gICAgfVxuICAgIGlmIChpc0Vycm9yKHZhbHVlKSkge1xuICAgICAgcmV0dXJuIGZvcm1hdEVycm9yKHZhbHVlKTtcbiAgICB9XG4gIH1cblxuICB2YXIgYmFzZSA9ICcnLCBhcnJheSA9IGZhbHNlLCBicmFjZXMgPSBbJ3snLCAnfSddO1xuXG4gIC8vIE1ha2UgQXJyYXkgc2F5IHRoYXQgdGhleSBhcmUgQXJyYXlcbiAgaWYgKGlzQXJyYXkodmFsdWUpKSB7XG4gICAgYXJyYXkgPSB0cnVlO1xuICAgIGJyYWNlcyA9IFsnWycsICddJ107XG4gIH1cblxuICAvLyBNYWtlIGZ1bmN0aW9ucyBzYXkgdGhhdCB0aGV5IGFyZSBmdW5jdGlvbnNcbiAgaWYgKGlzRnVuY3Rpb24odmFsdWUpKSB7XG4gICAgdmFyIG4gPSB2YWx1ZS5uYW1lID8gJzogJyArIHZhbHVlLm5hbWUgOiAnJztcbiAgICBiYXNlID0gJyBbRnVuY3Rpb24nICsgbiArICddJztcbiAgfVxuXG4gIC8vIE1ha2UgUmVnRXhwcyBzYXkgdGhhdCB0aGV5IGFyZSBSZWdFeHBzXG4gIGlmIChpc1JlZ0V4cCh2YWx1ZSkpIHtcbiAgICBiYXNlID0gJyAnICsgUmVnRXhwLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKTtcbiAgfVxuXG4gIC8vIE1ha2UgZGF0ZXMgd2l0aCBwcm9wZXJ0aWVzIGZpcnN0IHNheSB0aGUgZGF0ZVxuICBpZiAoaXNEYXRlKHZhbHVlKSkge1xuICAgIGJhc2UgPSAnICcgKyBEYXRlLnByb3RvdHlwZS50b1VUQ1N0cmluZy5jYWxsKHZhbHVlKTtcbiAgfVxuXG4gIC8vIE1ha2UgZXJyb3Igd2l0aCBtZXNzYWdlIGZpcnN0IHNheSB0aGUgZXJyb3JcbiAgaWYgKGlzRXJyb3IodmFsdWUpKSB7XG4gICAgYmFzZSA9ICcgJyArIGZvcm1hdEVycm9yKHZhbHVlKTtcbiAgfVxuXG4gIGlmIChrZXlzLmxlbmd0aCA9PT0gMCAmJiAoIWFycmF5IHx8IHZhbHVlLmxlbmd0aCA9PSAwKSkge1xuICAgIHJldHVybiBicmFjZXNbMF0gKyBiYXNlICsgYnJhY2VzWzFdO1xuICB9XG5cbiAgaWYgKHJlY3Vyc2VUaW1lcyA8IDApIHtcbiAgICBpZiAoaXNSZWdFeHAodmFsdWUpKSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoUmVnRXhwLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSwgJ3JlZ2V4cCcpO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gY3R4LnN0eWxpemUoJ1tPYmplY3RdJywgJ3NwZWNpYWwnKTtcbiAgICB9XG4gIH1cblxuICBjdHguc2Vlbi5wdXNoKHZhbHVlKTtcblxuICB2YXIgb3V0cHV0O1xuICBpZiAoYXJyYXkpIHtcbiAgICBvdXRwdXQgPSBmb3JtYXRBcnJheShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXlzKTtcbiAgfSBlbHNlIHtcbiAgICBvdXRwdXQgPSBrZXlzLm1hcChmdW5jdGlvbihrZXkpIHtcbiAgICAgIHJldHVybiBmb3JtYXRQcm9wZXJ0eShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXksIGFycmF5KTtcbiAgICB9KTtcbiAgfVxuXG4gIGN0eC5zZWVuLnBvcCgpO1xuXG4gIHJldHVybiByZWR1Y2VUb1NpbmdsZVN0cmluZyhvdXRwdXQsIGJhc2UsIGJyYWNlcyk7XG59XG5cblxuZnVuY3Rpb24gZm9ybWF0UHJpbWl0aXZlKGN0eCwgdmFsdWUpIHtcbiAgaWYgKGlzVW5kZWZpbmVkKHZhbHVlKSlcbiAgICByZXR1cm4gY3R4LnN0eWxpemUoJ3VuZGVmaW5lZCcsICd1bmRlZmluZWQnKTtcbiAgaWYgKGlzU3RyaW5nKHZhbHVlKSkge1xuICAgIHZhciBzaW1wbGUgPSAnXFwnJyArIEpTT04uc3RyaW5naWZ5KHZhbHVlKS5yZXBsYWNlKC9eXCJ8XCIkL2csICcnKVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnJlcGxhY2UoLycvZywgXCJcXFxcJ1wiKVxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgLnJlcGxhY2UoL1xcXFxcIi9nLCAnXCInKSArICdcXCcnO1xuICAgIHJldHVybiBjdHguc3R5bGl6ZShzaW1wbGUsICdzdHJpbmcnKTtcbiAgfVxuICBpZiAoaXNOdW1iZXIodmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnJyArIHZhbHVlLCAnbnVtYmVyJyk7XG4gIGlmIChpc0Jvb2xlYW4odmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnJyArIHZhbHVlLCAnYm9vbGVhbicpO1xuICAvLyBGb3Igc29tZSByZWFzb24gdHlwZW9mIG51bGwgaXMgXCJvYmplY3RcIiwgc28gc3BlY2lhbCBjYXNlIGhlcmUuXG4gIGlmIChpc051bGwodmFsdWUpKVxuICAgIHJldHVybiBjdHguc3R5bGl6ZSgnbnVsbCcsICdudWxsJyk7XG59XG5cblxuZnVuY3Rpb24gZm9ybWF0RXJyb3IodmFsdWUpIHtcbiAgcmV0dXJuICdbJyArIEVycm9yLnByb3RvdHlwZS50b1N0cmluZy5jYWxsKHZhbHVlKSArICddJztcbn1cblxuXG5mdW5jdGlvbiBmb3JtYXRBcnJheShjdHgsIHZhbHVlLCByZWN1cnNlVGltZXMsIHZpc2libGVLZXlzLCBrZXlzKSB7XG4gIHZhciBvdXRwdXQgPSBbXTtcbiAgZm9yICh2YXIgaSA9IDAsIGwgPSB2YWx1ZS5sZW5ndGg7IGkgPCBsOyArK2kpIHtcbiAgICBpZiAoaGFzT3duUHJvcGVydHkodmFsdWUsIFN0cmluZyhpKSkpIHtcbiAgICAgIG91dHB1dC5wdXNoKGZvcm1hdFByb3BlcnR5KGN0eCwgdmFsdWUsIHJlY3Vyc2VUaW1lcywgdmlzaWJsZUtleXMsXG4gICAgICAgICAgU3RyaW5nKGkpLCB0cnVlKSk7XG4gICAgfSBlbHNlIHtcbiAgICAgIG91dHB1dC5wdXNoKCcnKTtcbiAgICB9XG4gIH1cbiAga2V5cy5mb3JFYWNoKGZ1bmN0aW9uKGtleSkge1xuICAgIGlmICgha2V5Lm1hdGNoKC9eXFxkKyQvKSkge1xuICAgICAgb3V0cHV0LnB1c2goZm9ybWF0UHJvcGVydHkoY3R4LCB2YWx1ZSwgcmVjdXJzZVRpbWVzLCB2aXNpYmxlS2V5cyxcbiAgICAgICAgICBrZXksIHRydWUpKTtcbiAgICB9XG4gIH0pO1xuICByZXR1cm4gb3V0cHV0O1xufVxuXG5cbmZ1bmN0aW9uIGZvcm1hdFByb3BlcnR5KGN0eCwgdmFsdWUsIHJlY3Vyc2VUaW1lcywgdmlzaWJsZUtleXMsIGtleSwgYXJyYXkpIHtcbiAgdmFyIG5hbWUsIHN0ciwgZGVzYztcbiAgZGVzYyA9IE9iamVjdC5nZXRPd25Qcm9wZXJ0eURlc2NyaXB0b3IodmFsdWUsIGtleSkgfHwgeyB2YWx1ZTogdmFsdWVba2V5XSB9O1xuICBpZiAoZGVzYy5nZXQpIHtcbiAgICBpZiAoZGVzYy5zZXQpIHtcbiAgICAgIHN0ciA9IGN0eC5zdHlsaXplKCdbR2V0dGVyL1NldHRlcl0nLCAnc3BlY2lhbCcpO1xuICAgIH0gZWxzZSB7XG4gICAgICBzdHIgPSBjdHguc3R5bGl6ZSgnW0dldHRlcl0nLCAnc3BlY2lhbCcpO1xuICAgIH1cbiAgfSBlbHNlIHtcbiAgICBpZiAoZGVzYy5zZXQpIHtcbiAgICAgIHN0ciA9IGN0eC5zdHlsaXplKCdbU2V0dGVyXScsICdzcGVjaWFsJyk7XG4gICAgfVxuICB9XG4gIGlmICghaGFzT3duUHJvcGVydHkodmlzaWJsZUtleXMsIGtleSkpIHtcbiAgICBuYW1lID0gJ1snICsga2V5ICsgJ10nO1xuICB9XG4gIGlmICghc3RyKSB7XG4gICAgaWYgKGN0eC5zZWVuLmluZGV4T2YoZGVzYy52YWx1ZSkgPCAwKSB7XG4gICAgICBpZiAoaXNOdWxsKHJlY3Vyc2VUaW1lcykpIHtcbiAgICAgICAgc3RyID0gZm9ybWF0VmFsdWUoY3R4LCBkZXNjLnZhbHVlLCBudWxsKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHN0ciA9IGZvcm1hdFZhbHVlKGN0eCwgZGVzYy52YWx1ZSwgcmVjdXJzZVRpbWVzIC0gMSk7XG4gICAgICB9XG4gICAgICBpZiAoc3RyLmluZGV4T2YoJ1xcbicpID4gLTEpIHtcbiAgICAgICAgaWYgKGFycmF5KSB7XG4gICAgICAgICAgc3RyID0gc3RyLnNwbGl0KCdcXG4nKS5tYXAoZnVuY3Rpb24obGluZSkge1xuICAgICAgICAgICAgcmV0dXJuICcgICcgKyBsaW5lO1xuICAgICAgICAgIH0pLmpvaW4oJ1xcbicpLnN1YnN0cigyKTtcbiAgICAgICAgfSBlbHNlIHtcbiAgICAgICAgICBzdHIgPSAnXFxuJyArIHN0ci5zcGxpdCgnXFxuJykubWFwKGZ1bmN0aW9uKGxpbmUpIHtcbiAgICAgICAgICAgIHJldHVybiAnICAgJyArIGxpbmU7XG4gICAgICAgICAgfSkuam9pbignXFxuJyk7XG4gICAgICAgIH1cbiAgICAgIH1cbiAgICB9IGVsc2Uge1xuICAgICAgc3RyID0gY3R4LnN0eWxpemUoJ1tDaXJjdWxhcl0nLCAnc3BlY2lhbCcpO1xuICAgIH1cbiAgfVxuICBpZiAoaXNVbmRlZmluZWQobmFtZSkpIHtcbiAgICBpZiAoYXJyYXkgJiYga2V5Lm1hdGNoKC9eXFxkKyQvKSkge1xuICAgICAgcmV0dXJuIHN0cjtcbiAgICB9XG4gICAgbmFtZSA9IEpTT04uc3RyaW5naWZ5KCcnICsga2V5KTtcbiAgICBpZiAobmFtZS5tYXRjaCgvXlwiKFthLXpBLVpfXVthLXpBLVpfMC05XSopXCIkLykpIHtcbiAgICAgIG5hbWUgPSBuYW1lLnN1YnN0cigxLCBuYW1lLmxlbmd0aCAtIDIpO1xuICAgICAgbmFtZSA9IGN0eC5zdHlsaXplKG5hbWUsICduYW1lJyk7XG4gICAgfSBlbHNlIHtcbiAgICAgIG5hbWUgPSBuYW1lLnJlcGxhY2UoLycvZywgXCJcXFxcJ1wiKVxuICAgICAgICAgICAgICAgICAucmVwbGFjZSgvXFxcXFwiL2csICdcIicpXG4gICAgICAgICAgICAgICAgIC5yZXBsYWNlKC8oXlwifFwiJCkvZywgXCInXCIpO1xuICAgICAgbmFtZSA9IGN0eC5zdHlsaXplKG5hbWUsICdzdHJpbmcnKTtcbiAgICB9XG4gIH1cblxuICByZXR1cm4gbmFtZSArICc6ICcgKyBzdHI7XG59XG5cblxuZnVuY3Rpb24gcmVkdWNlVG9TaW5nbGVTdHJpbmcob3V0cHV0LCBiYXNlLCBicmFjZXMpIHtcbiAgdmFyIG51bUxpbmVzRXN0ID0gMDtcbiAgdmFyIGxlbmd0aCA9IG91dHB1dC5yZWR1Y2UoZnVuY3Rpb24ocHJldiwgY3VyKSB7XG4gICAgbnVtTGluZXNFc3QrKztcbiAgICBpZiAoY3VyLmluZGV4T2YoJ1xcbicpID49IDApIG51bUxpbmVzRXN0Kys7XG4gICAgcmV0dXJuIHByZXYgKyBjdXIucmVwbGFjZSgvXFx1MDAxYlxcW1xcZFxcZD9tL2csICcnKS5sZW5ndGggKyAxO1xuICB9LCAwKTtcblxuICBpZiAobGVuZ3RoID4gNjApIHtcbiAgICByZXR1cm4gYnJhY2VzWzBdICtcbiAgICAgICAgICAgKGJhc2UgPT09ICcnID8gJycgOiBiYXNlICsgJ1xcbiAnKSArXG4gICAgICAgICAgICcgJyArXG4gICAgICAgICAgIG91dHB1dC5qb2luKCcsXFxuICAnKSArXG4gICAgICAgICAgICcgJyArXG4gICAgICAgICAgIGJyYWNlc1sxXTtcbiAgfVxuXG4gIHJldHVybiBicmFjZXNbMF0gKyBiYXNlICsgJyAnICsgb3V0cHV0LmpvaW4oJywgJykgKyAnICcgKyBicmFjZXNbMV07XG59XG5cblxuLy8gTk9URTogVGhlc2UgdHlwZSBjaGVja2luZyBmdW5jdGlvbnMgaW50ZW50aW9uYWxseSBkb24ndCB1c2UgYGluc3RhbmNlb2ZgXG4vLyBiZWNhdXNlIGl0IGlzIGZyYWdpbGUgYW5kIGNhbiBiZSBlYXNpbHkgZmFrZWQgd2l0aCBgT2JqZWN0LmNyZWF0ZSgpYC5cbmZ1bmN0aW9uIGlzQXJyYXkoYXIpIHtcbiAgcmV0dXJuIEFycmF5LmlzQXJyYXkoYXIpO1xufVxuZXhwb3J0cy5pc0FycmF5ID0gaXNBcnJheTtcblxuZnVuY3Rpb24gaXNCb29sZWFuKGFyZykge1xuICByZXR1cm4gdHlwZW9mIGFyZyA9PT0gJ2Jvb2xlYW4nO1xufVxuZXhwb3J0cy5pc0Jvb2xlYW4gPSBpc0Jvb2xlYW47XG5cbmZ1bmN0aW9uIGlzTnVsbChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PT0gbnVsbDtcbn1cbmV4cG9ydHMuaXNOdWxsID0gaXNOdWxsO1xuXG5mdW5jdGlvbiBpc051bGxPclVuZGVmaW5lZChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PSBudWxsO1xufVxuZXhwb3J0cy5pc051bGxPclVuZGVmaW5lZCA9IGlzTnVsbE9yVW5kZWZpbmVkO1xuXG5mdW5jdGlvbiBpc051bWJlcihhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdudW1iZXInO1xufVxuZXhwb3J0cy5pc051bWJlciA9IGlzTnVtYmVyO1xuXG5mdW5jdGlvbiBpc1N0cmluZyhhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdzdHJpbmcnO1xufVxuZXhwb3J0cy5pc1N0cmluZyA9IGlzU3RyaW5nO1xuXG5mdW5jdGlvbiBpc1N5bWJvbChhcmcpIHtcbiAgcmV0dXJuIHR5cGVvZiBhcmcgPT09ICdzeW1ib2wnO1xufVxuZXhwb3J0cy5pc1N5bWJvbCA9IGlzU3ltYm9sO1xuXG5mdW5jdGlvbiBpc1VuZGVmaW5lZChhcmcpIHtcbiAgcmV0dXJuIGFyZyA9PT0gdm9pZCAwO1xufVxuZXhwb3J0cy5pc1VuZGVmaW5lZCA9IGlzVW5kZWZpbmVkO1xuXG5mdW5jdGlvbiBpc1JlZ0V4cChyZSkge1xuICByZXR1cm4gaXNPYmplY3QocmUpICYmIG9iamVjdFRvU3RyaW5nKHJlKSA9PT0gJ1tvYmplY3QgUmVnRXhwXSc7XG59XG5leHBvcnRzLmlzUmVnRXhwID0gaXNSZWdFeHA7XG5cbmZ1bmN0aW9uIGlzT2JqZWN0KGFyZykge1xuICByZXR1cm4gdHlwZW9mIGFyZyA9PT0gJ29iamVjdCcgJiYgYXJnICE9PSBudWxsO1xufVxuZXhwb3J0cy5pc09iamVjdCA9IGlzT2JqZWN0O1xuXG5mdW5jdGlvbiBpc0RhdGUoZCkge1xuICByZXR1cm4gaXNPYmplY3QoZCkgJiYgb2JqZWN0VG9TdHJpbmcoZCkgPT09ICdbb2JqZWN0IERhdGVdJztcbn1cbmV4cG9ydHMuaXNEYXRlID0gaXNEYXRlO1xuXG5mdW5jdGlvbiBpc0Vycm9yKGUpIHtcbiAgcmV0dXJuIGlzT2JqZWN0KGUpICYmXG4gICAgICAob2JqZWN0VG9TdHJpbmcoZSkgPT09ICdbb2JqZWN0IEVycm9yXScgfHwgZSBpbnN0YW5jZW9mIEVycm9yKTtcbn1cbmV4cG9ydHMuaXNFcnJvciA9IGlzRXJyb3I7XG5cbmZ1bmN0aW9uIGlzRnVuY3Rpb24oYXJnKSB7XG4gIHJldHVybiB0eXBlb2YgYXJnID09PSAnZnVuY3Rpb24nO1xufVxuZXhwb3J0cy5pc0Z1bmN0aW9uID0gaXNGdW5jdGlvbjtcblxuZnVuY3Rpb24gaXNQcmltaXRpdmUoYXJnKSB7XG4gIHJldHVybiBhcmcgPT09IG51bGwgfHxcbiAgICAgICAgIHR5cGVvZiBhcmcgPT09ICdib29sZWFuJyB8fFxuICAgICAgICAgdHlwZW9mIGFyZyA9PT0gJ251bWJlcicgfHxcbiAgICAgICAgIHR5cGVvZiBhcmcgPT09ICdzdHJpbmcnIHx8XG4gICAgICAgICB0eXBlb2YgYXJnID09PSAnc3ltYm9sJyB8fCAgLy8gRVM2IHN5bWJvbFxuICAgICAgICAgdHlwZW9mIGFyZyA9PT0gJ3VuZGVmaW5lZCc7XG59XG5leHBvcnRzLmlzUHJpbWl0aXZlID0gaXNQcmltaXRpdmU7XG5cbmV4cG9ydHMuaXNCdWZmZXIgPSByZXF1aXJlKCcuL3N1cHBvcnQvaXNCdWZmZXInKTtcblxuZnVuY3Rpb24gb2JqZWN0VG9TdHJpbmcobykge1xuICByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS50b1N0cmluZy5jYWxsKG8pO1xufVxuXG5cbmZ1bmN0aW9uIHBhZChuKSB7XG4gIHJldHVybiBuIDwgMTAgPyAnMCcgKyBuLnRvU3RyaW5nKDEwKSA6IG4udG9TdHJpbmcoMTApO1xufVxuXG5cbnZhciBtb250aHMgPSBbJ0phbicsICdGZWInLCAnTWFyJywgJ0FwcicsICdNYXknLCAnSnVuJywgJ0p1bCcsICdBdWcnLCAnU2VwJyxcbiAgICAgICAgICAgICAgJ09jdCcsICdOb3YnLCAnRGVjJ107XG5cbi8vIDI2IEZlYiAxNjoxOTozNFxuZnVuY3Rpb24gdGltZXN0YW1wKCkge1xuICB2YXIgZCA9IG5ldyBEYXRlKCk7XG4gIHZhciB0aW1lID0gW3BhZChkLmdldEhvdXJzKCkpLFxuICAgICAgICAgICAgICBwYWQoZC5nZXRNaW51dGVzKCkpLFxuICAgICAgICAgICAgICBwYWQoZC5nZXRTZWNvbmRzKCkpXS5qb2luKCc6Jyk7XG4gIHJldHVybiBbZC5nZXREYXRlKCksIG1vbnRoc1tkLmdldE1vbnRoKCldLCB0aW1lXS5qb2luKCcgJyk7XG59XG5cblxuLy8gbG9nIGlzIGp1c3QgYSB0aGluIHdyYXBwZXIgdG8gY29uc29sZS5sb2cgdGhhdCBwcmVwZW5kcyBhIHRpbWVzdGFtcFxuZXhwb3J0cy5sb2cgPSBmdW5jdGlvbigpIHtcbiAgY29uc29sZS5sb2coJyVzIC0gJXMnLCB0aW1lc3RhbXAoKSwgZXhwb3J0cy5mb3JtYXQuYXBwbHkoZXhwb3J0cywgYXJndW1lbnRzKSk7XG59O1xuXG5cbi8qKlxuICogSW5oZXJpdCB0aGUgcHJvdG90eXBlIG1ldGhvZHMgZnJvbSBvbmUgY29uc3RydWN0b3IgaW50byBhbm90aGVyLlxuICpcbiAqIFRoZSBGdW5jdGlvbi5wcm90b3R5cGUuaW5oZXJpdHMgZnJvbSBsYW5nLmpzIHJld3JpdHRlbiBhcyBhIHN0YW5kYWxvbmVcbiAqIGZ1bmN0aW9uIChub3Qgb24gRnVuY3Rpb24ucHJvdG90eXBlKS4gTk9URTogSWYgdGhpcyBmaWxlIGlzIHRvIGJlIGxvYWRlZFxuICogZHVyaW5nIGJvb3RzdHJhcHBpbmcgdGhpcyBmdW5jdGlvbiBuZWVkcyB0byBiZSByZXdyaXR0ZW4gdXNpbmcgc29tZSBuYXRpdmVcbiAqIGZ1bmN0aW9ucyBhcyBwcm90b3R5cGUgc2V0dXAgdXNpbmcgbm9ybWFsIEphdmFTY3JpcHQgZG9lcyBub3Qgd29yayBhc1xuICogZXhwZWN0ZWQgZHVyaW5nIGJvb3RzdHJhcHBpbmcgKHNlZSBtaXJyb3IuanMgaW4gcjExNDkwMykuXG4gKlxuICogQHBhcmFtIHtmdW5jdGlvbn0gY3RvciBDb25zdHJ1Y3RvciBmdW5jdGlvbiB3aGljaCBuZWVkcyB0byBpbmhlcml0IHRoZVxuICogICAgIHByb3RvdHlwZS5cbiAqIEBwYXJhbSB7ZnVuY3Rpb259IHN1cGVyQ3RvciBDb25zdHJ1Y3RvciBmdW5jdGlvbiB0byBpbmhlcml0IHByb3RvdHlwZSBmcm9tLlxuICovXG5leHBvcnRzLmluaGVyaXRzID0gcmVxdWlyZSgnaW5oZXJpdHMnKTtcblxuZXhwb3J0cy5fZXh0ZW5kID0gZnVuY3Rpb24ob3JpZ2luLCBhZGQpIHtcbiAgLy8gRG9uJ3QgZG8gYW55dGhpbmcgaWYgYWRkIGlzbid0IGFuIG9iamVjdFxuICBpZiAoIWFkZCB8fCAhaXNPYmplY3QoYWRkKSkgcmV0dXJuIG9yaWdpbjtcblxuICB2YXIga2V5cyA9IE9iamVjdC5rZXlzKGFkZCk7XG4gIHZhciBpID0ga2V5cy5sZW5ndGg7XG4gIHdoaWxlIChpLS0pIHtcbiAgICBvcmlnaW5ba2V5c1tpXV0gPSBhZGRba2V5c1tpXV07XG4gIH1cbiAgcmV0dXJuIG9yaWdpbjtcbn07XG5cbmZ1bmN0aW9uIGhhc093blByb3BlcnR5KG9iaiwgcHJvcCkge1xuICByZXR1cm4gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsKG9iaiwgcHJvcCk7XG59XG4iLCIvKipcclxuICogQ3JlYXRlZCBieSByaWNoYXJkLmxpdmluZ3N0b24gb24gMTgvMDIvMjAxNy5cclxuICovXHJcbid1c2Ugc3RyaWN0JztcclxuXHJcbnZhciB1dGlsID0gcmVxdWlyZSgndXRpbCcpLFxyXG5cdEVFID0gcmVxdWlyZSgnZXZlbnRzJykuRXZlbnRFbWl0dGVyO1xyXG5cclxuXHJcbm1vZHVsZS5leHBvcnRzID0gSlNPTlZpZXc7XHJcbnV0aWwuaW5oZXJpdHMoSlNPTlZpZXcsIEVFKTtcclxuXHJcblxyXG5mdW5jdGlvbiBKU09OVmlldyhuYW1lXywgdmFsdWVfKXtcclxuXHR2YXIgc2VsZiA9IHRoaXM7XHJcblxyXG5cdEVFLmNhbGwoc2VsZik7XHJcblxyXG5cdGlmKGFyZ3VtZW50cy5sZW5ndGggPCAyKXtcclxuXHRcdHZhbHVlXyA9IG5hbWVfO1xyXG5cdFx0bmFtZV8gPSB1bmRlZmluZWQ7XHJcblx0fVxyXG5cclxuXHR2YXIgbmFtZSwgdmFsdWUsIHR5cGUsXHJcblx0XHRkb21FdmVudExpc3RlbmVycyA9IFtdLCBjaGlsZHJlbiA9IFtdLCBleHBhbmRlZCA9IGZhbHNlLFxyXG5cdFx0ZWRpdHRpbmdOYW1lID0gZmFsc2UsIGVkaXR0aW5nVmFsdWUgPSBmYWxzZSxcclxuXHRcdG5hbWVFZGl0YWJsZSA9IHRydWUsIHZhbHVlRWRpdGFibGUgPSB0cnVlO1xyXG5cclxuXHR2YXIgZG9tID0ge1xyXG5cdFx0Y29udGFpbmVyIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHRjb2xsYXBzZUV4cGFuZCA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0bmFtZSA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0c2VwYXJhdG9yIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHR2YWx1ZSA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0ZGVsZXRlIDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JyksXHJcblx0XHRjaGlsZHJlbiA6IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpLFxyXG5cdFx0aW5zZXJ0IDogZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2JylcclxuXHR9O1xyXG5cclxuXHJcblx0T2JqZWN0LmRlZmluZVByb3BlcnRpZXMoc2VsZiwge1xyXG5cclxuXHRcdGRvbSA6IHtcclxuXHRcdFx0dmFsdWUgOiBkb20uY29udGFpbmVyLFxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fSxcclxuXHJcblx0XHRuYW1lIDoge1xyXG5cdFx0XHRnZXQgOiBmdW5jdGlvbigpe1xyXG5cdFx0XHRcdHJldHVybiBuYW1lO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0c2V0IDogc2V0TmFtZSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0dmFsdWUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIHZhbHVlO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0c2V0IDogc2V0VmFsdWUsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdHR5cGUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIHR5cGU7XHJcblx0XHRcdH0sXHJcblxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fSxcclxuXHJcblx0XHRuYW1lRWRpdGFibGUgOiB7XHJcblx0XHRcdGdldCA6IGZ1bmN0aW9uKCl7XHJcblx0XHRcdFx0cmV0dXJuIG5hbWVFZGl0YWJsZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdHNldCA6IGZ1bmN0aW9uKHZhbHVlKXtcclxuXHRcdFx0XHRuYW1lRWRpdGFibGUgPSAhIXZhbHVlO1xyXG5cdFx0XHR9LFxyXG5cclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0dmFsdWVFZGl0YWJsZSA6IHtcclxuXHRcdFx0Z2V0IDogZnVuY3Rpb24oKXtcclxuXHRcdFx0XHRyZXR1cm4gdmFsdWVFZGl0YWJsZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdHNldCA6IGZ1bmN0aW9uKHZhbHVlKXtcclxuXHRcdFx0XHR2YWx1ZUVkaXRhYmxlID0gISF2YWx1ZTtcclxuXHRcdFx0fSxcclxuXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdHJlZnJlc2ggOiB7XHJcblx0XHRcdHZhbHVlIDogcmVmcmVzaCxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0Y29sbGFwc2UgOiB7XHJcblx0XHRcdHZhbHVlIDogY29sbGFwc2UsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdGV4cGFuZCA6IHtcclxuXHRcdFx0dmFsdWUgOiBleHBhbmQsXHJcblx0XHRcdGVudW1lcmFibGUgOiB0cnVlXHJcblx0XHR9LFxyXG5cclxuXHRcdGRlc3Ryb3kgOiB7XHJcblx0XHRcdHZhbHVlIDogZGVzdHJveSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0ZWRpdE5hbWUgOiB7XHJcblx0XHRcdHZhbHVlIDogZWRpdEZpZWxkLmJpbmQobnVsbCwgJ25hbWUnKSxcclxuXHRcdFx0ZW51bWVyYWJsZSA6IHRydWVcclxuXHRcdH0sXHJcblxyXG5cdFx0ZWRpdFZhbHVlIDoge1xyXG5cdFx0XHR2YWx1ZSA6IGVkaXRGaWVsZC5iaW5kKG51bGwsICd2YWx1ZScpLFxyXG5cdFx0XHRlbnVtZXJhYmxlIDogdHJ1ZVxyXG5cdFx0fVxyXG5cclxuXHR9KTtcclxuXHJcblxyXG5cdE9iamVjdC5rZXlzKGRvbSkuZm9yRWFjaChmdW5jdGlvbihrKXtcclxuXHRcdHZhciBlbGVtZW50ID0gZG9tW2tdO1xyXG5cclxuXHRcdGlmKGsgPT0gJ2NvbnRhaW5lcicpe1xyXG5cdFx0XHRyZXR1cm47XHJcblx0XHR9XHJcblxyXG5cdFx0ZWxlbWVudC5jbGFzc05hbWUgPSBrO1xyXG5cdFx0ZG9tLmNvbnRhaW5lci5hcHBlbmRDaGlsZChlbGVtZW50KTtcclxuXHR9KTtcclxuXHJcblx0ZG9tLmNvbnRhaW5lci5jbGFzc05hbWUgPSAnanNvblZpZXcnO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5jb2xsYXBzZUV4cGFuZCwgJ2NsaWNrJywgb25Db2xsYXBzZUV4cGFuZENsaWNrKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2NsaWNrJywgZXhwYW5kLmJpbmQobnVsbCwgZmFsc2UpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnY2xpY2snLCBleHBhbmQuYmluZChudWxsLCBmYWxzZSkpO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnZGJsY2xpY2snLCBlZGl0RmllbGQuYmluZChudWxsLCAnbmFtZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAnYmx1cicsIGVkaXRGaWVsZFN0b3AuYmluZChudWxsLCAnbmFtZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5uYW1lLCAna2V5cHJlc3MnLCBlZGl0RmllbGRLZXlQcmVzc2VkLmJpbmQobnVsbCwgJ25hbWUnKSk7XHJcblx0YWRkRG9tRXZlbnRMaXN0ZW5lcihkb20ubmFtZSwgJ2tleWRvd24nLCBlZGl0RmllbGRUYWJQcmVzc2VkLmJpbmQobnVsbCwgJ25hbWUnKSk7XHJcblxyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLnZhbHVlLCAnZGJsY2xpY2snLCBlZGl0RmllbGQuYmluZChudWxsLCAndmFsdWUnKSk7XHJcblx0YWRkRG9tRXZlbnRMaXN0ZW5lcihkb20udmFsdWUsICdibHVyJywgZWRpdEZpZWxkU3RvcC5iaW5kKG51bGwsICd2YWx1ZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2tleXByZXNzJywgZWRpdEZpZWxkS2V5UHJlc3NlZC5iaW5kKG51bGwsICd2YWx1ZScpKTtcclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS52YWx1ZSwgJ2tleWRvd24nLCBlZGl0RmllbGRUYWJQcmVzc2VkLmJpbmQobnVsbCwgJ3ZhbHVlJykpO1xyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLnZhbHVlLCAna2V5ZG93bicsIG51bWVyaWNWYWx1ZUtleURvd24pO1xyXG5cclxuXHRhZGREb21FdmVudExpc3RlbmVyKGRvbS5pbnNlcnQsICdjbGljaycsIG9uSW5zZXJ0Q2xpY2spO1xyXG5cdGFkZERvbUV2ZW50TGlzdGVuZXIoZG9tLmRlbGV0ZSwgJ2NsaWNrJywgb25EZWxldGVDbGljayk7XHJcblxyXG5cdHNldE5hbWUobmFtZV8pO1xyXG5cdHNldFZhbHVlKHZhbHVlXyk7XHJcblxyXG5cclxuXHRmdW5jdGlvbiByZWZyZXNoKCl7XHJcblx0XHR2YXIgZXhwYW5kYWJsZSA9IHR5cGUgPT0gJ29iamVjdCcgfHwgdHlwZSA9PSAnYXJyYXknO1xyXG5cclxuXHRcdGNoaWxkcmVuLmZvckVhY2goZnVuY3Rpb24oY2hpbGQpe1xyXG5cdFx0XHRjaGlsZC5yZWZyZXNoKCk7XHJcblx0XHR9KTtcclxuXHJcblx0XHRkb20uY29sbGFwc2VFeHBhbmQuc3R5bGUuZGlzcGxheSA9IGV4cGFuZGFibGUgPyAnJyA6ICdub25lJztcclxuXHJcblx0XHRpZihleHBhbmRlZCAmJiBleHBhbmRhYmxlKXtcclxuXHRcdFx0ZXhwYW5kKCk7XHJcblx0XHR9XHJcblx0XHRlbHNle1xyXG5cdFx0XHRjb2xsYXBzZSgpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGNvbGxhcHNlKHJlY3Vyc2l2ZSl7XHJcblx0XHRpZihyZWN1cnNpdmUpe1xyXG5cdFx0XHRjaGlsZHJlbi5mb3JFYWNoKGZ1bmN0aW9uKGNoaWxkKXtcclxuXHRcdFx0XHRjaGlsZC5jb2xsYXBzZSh0cnVlKTtcclxuXHRcdFx0fSk7XHJcblx0XHR9XHJcblxyXG5cdFx0ZXhwYW5kZWQgPSBmYWxzZTtcclxuXHJcblx0XHRkb20uY2hpbGRyZW4uc3R5bGUuZGlzcGxheSA9ICdub25lJztcclxuXHRcdGRvbS5jb2xsYXBzZUV4cGFuZC5jbGFzc05hbWUgPSAnZXhwYW5kJztcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LmFkZCgnY29sbGFwc2VkJyk7XHJcblx0XHRkb20uY29udGFpbmVyLmNsYXNzTGlzdC5yZW1vdmUoJ2V4cGFuZGVkJyk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZXhwYW5kKHJlY3Vyc2l2ZSl7XHJcblx0XHR2YXIga2V5cztcclxuXHJcblx0XHRpZih0eXBlID09ICdvYmplY3QnKXtcclxuXHRcdFx0a2V5cyA9IE9iamVjdC5rZXlzKHZhbHVlKTtcclxuXHRcdH1cclxuXHRcdGVsc2UgaWYodHlwZSA9PSAnYXJyYXknKXtcclxuXHRcdFx0a2V5cyA9IHZhbHVlLm1hcChmdW5jdGlvbih2LCBrKXtcclxuXHRcdFx0XHRyZXR1cm4gaztcclxuXHRcdFx0fSk7XHJcblx0XHR9XHJcblx0XHRlbHNle1xyXG5cdFx0XHRrZXlzID0gW107XHJcblx0XHR9XHJcblxyXG5cdFx0Ly8gUmVtb3ZlIGNoaWxkcmVuIHRoYXQgbm8gbG9uZ2VyIGV4aXN0XHJcblx0XHRmb3IodmFyIGkgPSBjaGlsZHJlbi5sZW5ndGggLSAxOyBpID49IDA7IGkgLS0pe1xyXG5cdFx0XHR2YXIgY2hpbGQgPSBjaGlsZHJlbltpXTtcclxuXHJcblx0XHRcdGlmKGtleXMuaW5kZXhPZihjaGlsZC5uYW1lKSA9PSAtMSl7XHJcblx0XHRcdFx0Y2hpbGRyZW4uc3BsaWNlKGksIDEpO1xyXG5cdFx0XHRcdHJlbW92ZUNoaWxkKGNoaWxkKTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cclxuXHRcdGlmKHR5cGUgIT0gJ29iamVjdCcgJiYgdHlwZSAhPSAnYXJyYXknKXtcclxuXHRcdFx0cmV0dXJuIGNvbGxhcHNlKCk7XHJcblx0XHR9XHJcblxyXG5cdFx0a2V5cy5mb3JFYWNoKGZ1bmN0aW9uKGtleSl7XHJcblx0XHRcdGFkZENoaWxkKGtleSwgdmFsdWVba2V5XSk7XHJcblx0XHR9KTtcclxuXHJcblx0XHRpZihyZWN1cnNpdmUpe1xyXG5cdFx0XHRjaGlsZHJlbi5mb3JFYWNoKGZ1bmN0aW9uKGNoaWxkKXtcclxuXHRcdFx0XHRjaGlsZC5leHBhbmQodHJ1ZSk7XHJcblx0XHRcdH0pO1xyXG5cdFx0fVxyXG5cclxuXHRcdGV4cGFuZGVkID0gdHJ1ZTtcclxuXHRcdGRvbS5jaGlsZHJlbi5zdHlsZS5kaXNwbGF5ID0gJyc7XHJcblx0XHRkb20uY29sbGFwc2VFeHBhbmQuY2xhc3NOYW1lID0gJ2NvbGxhcHNlJztcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LmFkZCgnZXhwYW5kZWQnKTtcclxuXHRcdGRvbS5jb250YWluZXIuY2xhc3NMaXN0LnJlbW92ZSgnY29sbGFwc2VkJyk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZGVzdHJveSgpe1xyXG5cdFx0dmFyIGNoaWxkLCBldmVudDtcclxuXHJcblx0XHR3aGlsZShldmVudCA9IGRvbUV2ZW50TGlzdGVuZXJzLnBvcCgpKXtcclxuXHRcdFx0ZXZlbnQuZWxlbWVudC5yZW1vdmVFdmVudExpc3RlbmVyKGV2ZW50Lm5hbWUsIGV2ZW50LmZuKTtcclxuXHRcdH1cclxuXHJcblx0XHR3aGlsZShjaGlsZCA9IGNoaWxkcmVuLnBvcCgpKXtcclxuXHRcdFx0cmVtb3ZlQ2hpbGQoY2hpbGQpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIHNldE5hbWUobmV3TmFtZSl7XHJcblx0XHR2YXIgbmFtZVR5cGUgPSB0eXBlb2YgbmV3TmFtZSxcclxuXHRcdFx0b2xkTmFtZSA9IG5hbWU7XHJcblxyXG5cdFx0aWYobmV3TmFtZSA9PT0gbmFtZSl7XHJcblx0XHRcdHJldHVybjtcclxuXHRcdH1cclxuXHJcblx0XHRpZihuYW1lVHlwZSAhPSAnc3RyaW5nJyAmJiBuYW1lVHlwZSAhPSAnbnVtYmVyJyl7XHJcblx0XHRcdHRocm93IG5ldyBFcnJvcignTmFtZSBtdXN0IGJlIGVpdGhlciBzdHJpbmcgb3IgbnVtYmVyLCAnICsgbmV3TmFtZSk7XHJcblx0XHR9XHJcblxyXG5cdFx0ZG9tLm5hbWUuaW5uZXJUZXh0ID0gbmV3TmFtZTtcclxuXHRcdG5hbWUgPSBuZXdOYW1lO1xyXG5cdFx0c2VsZi5lbWl0KCdyZW5hbWUnLCBzZWxmLCBvbGROYW1lLCBuZXdOYW1lKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBzZXRWYWx1ZShuZXdWYWx1ZSl7XHJcblx0XHR2YXIgb2xkVmFsdWUgPSB2YWx1ZSxcclxuXHRcdFx0c3RyO1xyXG5cclxuXHRcdHR5cGUgPSBnZXRUeXBlKG5ld1ZhbHVlKTtcclxuXHJcblx0XHRzd2l0Y2godHlwZSl7XHJcblx0XHRcdGNhc2UgJ251bGwnOlxyXG5cdFx0XHRcdHN0ciA9ICdudWxsJztcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdFx0Y2FzZSAnb2JqZWN0JzpcclxuXHRcdFx0XHRzdHIgPSAnT2JqZWN0WycgKyBPYmplY3Qua2V5cyhuZXdWYWx1ZSkubGVuZ3RoICsgJ10nO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cclxuXHRcdFx0Y2FzZSAnYXJyYXknOlxyXG5cdFx0XHRcdHN0ciA9ICdBcnJheVsnICsgbmV3VmFsdWUubGVuZ3RoICsgJ10nO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cclxuXHRcdFx0ZGVmYXVsdDpcclxuXHRcdFx0XHRzdHIgPSBuZXdWYWx1ZTtcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdH1cclxuXHJcblx0XHRkb20udmFsdWUuaW5uZXJUZXh0ID0gc3RyO1xyXG5cdFx0ZG9tLnZhbHVlLmNsYXNzTmFtZSA9ICd2YWx1ZSAnICsgdHlwZTtcclxuXHJcblx0XHRpZihuZXdWYWx1ZSA9PT0gdmFsdWUpe1xyXG5cdFx0XHRyZXR1cm47XHJcblx0XHR9XHJcblxyXG5cdFx0dmFsdWUgPSBuZXdWYWx1ZTtcclxuXHJcblx0XHRpZih0eXBlID09ICdhcnJheScgfHwgdHlwZSA9PSAnb2JqZWN0Jyl7XHJcblx0XHRcdC8vIENhbm5vdCBlZGl0IG9iamVjdHMgYXMgc3RyaW5nIGJlY2F1c2UgdGhlIGZvcm1hdHRpbmcgaXMgdG9vIG1lc3N5XHJcblx0XHRcdC8vIFdvdWxkIGhhdmUgdG8gZWl0aGVyIHBhc3MgYXMgSlNPTiBhbmQgZm9yY2UgdXNlciB0byB3cmFwIHByb3BlcnRpZXMgaW4gcXVvdGVzXHJcblx0XHRcdC8vIE9yIGZpcnN0IEpTT04gc3RyaW5naWZ5IHRoZSBpbnB1dCBiZWZvcmUgcGFzc2luZywgdGhpcyBjb3VsZCBhbGxvdyB1c2VycyB0byByZWZlcmVuY2UgZ2xvYmFsc1xyXG5cclxuXHRcdFx0Ly8gSW5zdGVhZCB0aGUgdXNlciBjYW4gbW9kaWZ5IGluZGl2aWR1YWwgcHJvcGVydGllcywgb3IganVzdCBkZWxldGUgdGhlIG9iamVjdCBhbmQgc3RhcnQgYWdhaW5cclxuXHRcdFx0dmFsdWVFZGl0YWJsZSA9IGZhbHNlO1xyXG5cclxuXHRcdFx0aWYodHlwZSA9PSAnYXJyYXknKXtcclxuXHRcdFx0XHQvLyBPYnZpb3VzbHkgY2Fubm90IG1vZGlmeSBhcnJheSBrZXlzXHJcblx0XHRcdFx0bmFtZUVkaXRhYmxlID0gZmFsc2U7XHJcblx0XHRcdH1cclxuXHRcdH1cclxuXHJcblx0XHRyZWZyZXNoKCk7XHJcblx0XHRzZWxmLmVtaXQoJ2NoYW5nZScsIG5hbWUsIG9sZFZhbHVlLCBuZXdWYWx1ZSk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gYWRkQ2hpbGQoa2V5LCB2YWwpe1xyXG5cdFx0dmFyIGNoaWxkO1xyXG5cclxuXHRcdGZvcih2YXIgaSA9IDAsIGxlbiA9IGNoaWxkcmVuLmxlbmd0aDsgaSA8IGxlbjsgaSArKyl7XHJcblx0XHRcdGlmKGNoaWxkcmVuW2ldLm5hbWUgPT0ga2V5KXtcclxuXHRcdFx0XHRjaGlsZCA9IGNoaWxkcmVuW2ldO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cdFx0XHR9XHJcblx0XHR9XHJcblxyXG5cdFx0aWYoY2hpbGQpe1xyXG5cdFx0XHRjaGlsZC52YWx1ZSA9IHZhbDtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdGNoaWxkID0gbmV3IEpTT05WaWV3KGtleSwgdmFsKTtcclxuXHRcdFx0Y2hpbGQub25jZSgncmVuYW1lJywgb25DaGlsZFJlbmFtZSk7XHJcblx0XHRcdGNoaWxkLm9uKCdkZWxldGUnLCBvbkNoaWxkRGVsZXRlKTtcclxuXHRcdFx0Y2hpbGQub24oJ2NoYW5nZScsIG9uQ2hpbGRDaGFuZ2UpO1xyXG5cdFx0XHRjaGlsZHJlbi5wdXNoKGNoaWxkKTtcclxuXHRcdH1cclxuXHJcblx0XHRkb20uY2hpbGRyZW4uYXBwZW5kQ2hpbGQoY2hpbGQuZG9tKTtcclxuXHJcblx0XHRyZXR1cm4gY2hpbGQ7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gcmVtb3ZlQ2hpbGQoY2hpbGQpe1xyXG5cdFx0aWYoY2hpbGQuZG9tLnBhcmVudE5vZGUpe1xyXG5cdFx0XHRkb20uY2hpbGRyZW4ucmVtb3ZlQ2hpbGQoY2hpbGQuZG9tKTtcclxuXHRcdH1cclxuXHJcblx0XHRjaGlsZC5kZXN0cm95KCk7XHJcblx0XHRjaGlsZC5yZW1vdmVBbGxMaXN0ZW5lcnMoKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBlZGl0RmllbGQoZmllbGQpe1xyXG5cdFx0dmFyIGVkaXRhYmxlID0gZmllbGQgPT0gJ25hbWUnID8gbmFtZUVkaXRhYmxlIDogdmFsdWVFZGl0YWJsZSxcclxuXHRcdFx0ZWxlbWVudCA9IGRvbVtmaWVsZF07XHJcblxyXG5cdFx0aWYoIWVkaXRhYmxlKXtcclxuXHRcdFx0cmV0dXJuO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGZpZWxkID09ICd2YWx1ZScgJiYgdHlwZSA9PSAnc3RyaW5nJyl7XHJcblx0XHRcdGVsZW1lbnQuaW5uZXJUZXh0ID0gJ1wiJyArIHZhbHVlICsgJ1wiJztcclxuXHRcdH1cclxuXHJcblx0XHRpZihmaWVsZCA9PSAnbmFtZScpe1xyXG5cdFx0XHRlZGl0dGluZ05hbWUgPSB0cnVlO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGZpZWxkID09ICd2YWx1ZScpe1xyXG5cdFx0XHRlZGl0dGluZ1ZhbHVlID0gdHJ1ZTtcclxuXHRcdH1cclxuXHJcblx0XHRlbGVtZW50LmNsYXNzTGlzdC5hZGQoJ2VkaXQnKTtcclxuXHRcdGVsZW1lbnQuc2V0QXR0cmlidXRlKCdjb250ZW50ZWRpdGFibGUnLCB0cnVlKTtcclxuXHRcdGVsZW1lbnQuZm9jdXMoKTtcclxuXHRcdGRvY3VtZW50LmV4ZWNDb21tYW5kKCdzZWxlY3RBbGwnLCBmYWxzZSwgbnVsbCk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gZWRpdEZpZWxkU3RvcChmaWVsZCl7XHJcblx0XHR2YXIgZWxlbWVudCA9IGRvbVtmaWVsZF07XHJcblx0XHRcclxuXHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdGlmKCFlZGl0dGluZ05hbWUpe1xyXG5cdFx0XHRcdHJldHVybjtcclxuXHRcdFx0fVxyXG5cdFx0XHRlZGl0dGluZ05hbWUgPSBmYWxzZTtcclxuXHRcdH1cclxuXHJcblx0XHRpZihmaWVsZCA9PSAndmFsdWUnKXtcclxuXHRcdFx0aWYoIWVkaXR0aW5nVmFsdWUpe1xyXG5cdFx0XHRcdHJldHVybjtcclxuXHRcdFx0fVxyXG5cdFx0XHRlZGl0dGluZ1ZhbHVlID0gZmFsc2U7XHJcblx0XHR9XHJcblx0XHRcclxuXHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdHNldE5hbWUoZWxlbWVudC5pbm5lclRleHQpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0dHJ5e1xyXG5cdFx0XHRcdHNldFZhbHVlKEpTT04ucGFyc2UoZWxlbWVudC5pbm5lclRleHQpKTtcclxuXHRcdFx0fVxyXG5cdFx0XHRjYXRjaChlcnIpe1xyXG5cdFx0XHRcdHNldFZhbHVlKGVsZW1lbnQuaW5uZXJUZXh0KTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cclxuXHRcdGVsZW1lbnQuY2xhc3NMaXN0LnJlbW92ZSgnZWRpdCcpO1xyXG5cdFx0ZWxlbWVudC5yZW1vdmVBdHRyaWJ1dGUoJ2NvbnRlbnRlZGl0YWJsZScpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGVkaXRGaWVsZEtleVByZXNzZWQoZmllbGQsIGUpe1xyXG5cdFx0c3dpdGNoKGUua2V5KXtcclxuXHRcdFx0Y2FzZSAnRXNjYXBlJzpcclxuXHRcdFx0Y2FzZSAnRW50ZXInOlxyXG5cdFx0XHRcdGVkaXRGaWVsZFN0b3AoZmllbGQpO1xyXG5cdFx0XHRcdGJyZWFrO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGVkaXRGaWVsZFRhYlByZXNzZWQoZmllbGQsIGUpe1xyXG5cdFx0aWYoZS5rZXkgPT0gJ1RhYicpe1xyXG5cdFx0XHRlZGl0RmllbGRTdG9wKGZpZWxkKTtcclxuXHJcblx0XHRcdGlmKGZpZWxkID09ICduYW1lJyl7XHJcblx0XHRcdFx0ZS5wcmV2ZW50RGVmYXVsdCgpO1xyXG5cdFx0XHRcdGVkaXRGaWVsZCgndmFsdWUnKTtcclxuXHRcdFx0fVxyXG5cdFx0XHRlbHNle1xyXG5cdFx0XHRcdGVkaXRGaWVsZFN0b3AoZmllbGQpO1xyXG5cdFx0XHR9XHJcblx0XHR9XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gbnVtZXJpY1ZhbHVlS2V5RG93bihlKXtcclxuXHRcdHZhciBpbmNyZW1lbnQgPSAwLCBjdXJyZW50VmFsdWU7XHJcblxyXG5cdFx0aWYodHlwZSAhPSAnbnVtYmVyJyl7XHJcblx0XHRcdHJldHVybjtcclxuXHRcdH1cclxuXHJcblx0XHRzd2l0Y2goZS5rZXkpe1xyXG5cdFx0XHRjYXNlICdBcnJvd0Rvd24nOlxyXG5cdFx0XHRjYXNlICdEb3duJzpcclxuXHRcdFx0XHRpbmNyZW1lbnQgPSAtMTtcclxuXHRcdFx0XHRicmVhaztcclxuXHJcblx0XHRcdGNhc2UgJ0Fycm93VXAnOlxyXG5cdFx0XHRjYXNlICdVcCc6XHJcblx0XHRcdFx0aW5jcmVtZW50ID0gMTtcclxuXHRcdFx0XHRicmVhaztcclxuXHRcdH1cclxuXHJcblx0XHRpZihlLnNoaWZ0S2V5KXtcclxuXHRcdFx0aW5jcmVtZW50ICo9IDEwO1xyXG5cdFx0fVxyXG5cclxuXHRcdGlmKGUuY3RybEtleSB8fCBlLm1ldGFLZXkpe1xyXG5cdFx0XHRpbmNyZW1lbnQgLz0gMTA7XHJcblx0XHR9XHJcblxyXG5cdFx0aWYoaW5jcmVtZW50KXtcclxuXHRcdFx0Y3VycmVudFZhbHVlID0gcGFyc2VGbG9hdChkb20udmFsdWUuaW5uZXJUZXh0KTtcclxuXHJcblx0XHRcdGlmKCFpc05hTihjdXJyZW50VmFsdWUpKXtcclxuXHRcdFx0XHRkb20udmFsdWUuaW5uZXJUZXh0ID0gTnVtYmVyKChjdXJyZW50VmFsdWUgKyBpbmNyZW1lbnQpLnRvRml4ZWQoMTApKTtcclxuXHRcdFx0fVxyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGdldFR5cGUodmFsdWUpe1xyXG5cdFx0dmFyIHR5cGUgPSB0eXBlb2YgdmFsdWU7XHJcblxyXG5cdFx0aWYodHlwZSA9PSAnb2JqZWN0Jyl7XHJcblx0XHRcdGlmKHZhbHVlID09PSBudWxsKXtcclxuXHRcdFx0XHRyZXR1cm4gJ251bGwnO1xyXG5cdFx0XHR9XHJcblxyXG5cdFx0XHRpZihBcnJheS5pc0FycmF5KHZhbHVlKSl7XHJcblx0XHRcdFx0cmV0dXJuICdhcnJheSc7XHJcblx0XHRcdH1cclxuXHRcdH1cclxuXHJcblx0XHRyZXR1cm4gdHlwZTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkNvbGxhcHNlRXhwYW5kQ2xpY2soKXtcclxuXHRcdGlmKGV4cGFuZGVkKXtcclxuXHRcdFx0Y29sbGFwc2UoKTtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdGV4cGFuZCgpO1xyXG5cdFx0fVxyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIG9uSW5zZXJ0Q2xpY2soKXtcclxuXHRcdHZhciBuZXdOYW1lID0gdHlwZSA9PSAnYXJyYXknID8gdmFsdWUubGVuZ3RoIDogdW5kZWZpbmVkLFxyXG5cdFx0XHRjaGlsZCA9IGFkZENoaWxkKG5ld05hbWUsIG51bGwpO1xyXG5cclxuXHRcdGlmKHR5cGUgPT0gJ2FycmF5Jyl7XHJcblx0XHRcdHZhbHVlLnB1c2gobnVsbCk7XHJcblx0XHRcdGNoaWxkLmVkaXRWYWx1ZSgpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0Y2hpbGQuZWRpdE5hbWUoKTtcclxuXHRcdH1cclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkRlbGV0ZUNsaWNrKCl7XHJcblx0XHRzZWxmLmVtaXQoJ2RlbGV0ZScsIHNlbGYpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIG9uQ2hpbGRSZW5hbWUoY2hpbGQsIG9sZE5hbWUsIG5ld05hbWUpe1xyXG5cdFx0dmFyIGFsbG93ID0gbmV3TmFtZSAmJiB0eXBlICE9ICdhcnJheScgJiYgIShuZXdOYW1lIGluIHZhbHVlKTtcclxuXHJcblx0XHRpZihhbGxvdyl7XHJcblx0XHRcdHZhbHVlW25ld05hbWVdID0gY2hpbGQudmFsdWU7XHJcblx0XHRcdGRlbGV0ZSB2YWx1ZVtvbGROYW1lXTtcclxuXHRcdH1cclxuXHRcdGVsc2UgaWYob2xkTmFtZSA9PT0gdW5kZWZpbmVkKXtcclxuXHRcdFx0Ly8gQSBuZXcgbm9kZSBpbnNlcnRlZCB2aWEgdGhlIFVJXHJcblx0XHRcdHJlbW92ZUNoaWxkKGNoaWxkKTtcclxuXHRcdH1cclxuXHRcdGVsc2V7XHJcblx0XHRcdC8vIENhbm5vdCByZW5hbWUgYXJyYXkga2V5cywgb3IgZHVwbGljYXRlIG9iamVjdCBrZXkgbmFtZXNcclxuXHRcdFx0Y2hpbGQubmFtZSA9IG9sZE5hbWU7XHJcblx0XHR9XHJcblxyXG5cdFx0Y2hpbGQub25jZSgncmVuYW1lJywgb25DaGlsZFJlbmFtZSk7XHJcblx0fVxyXG5cclxuXHJcblx0ZnVuY3Rpb24gb25DaGlsZENoYW5nZShrZXlQYXRoLCBvbGRWYWx1ZSwgbmV3VmFsdWUsIHJlY3Vyc2VkKXtcclxuXHRcdGlmKCFyZWN1cnNlZCl7XHJcblx0XHRcdHZhbHVlW2tleVBhdGhdID0gbmV3VmFsdWU7XHJcblx0XHR9XHJcblxyXG5cdFx0c2VsZi5lbWl0KCdjaGFuZ2UnLCBuYW1lICsgJy4nICsga2V5UGF0aCwgb2xkVmFsdWUsIG5ld1ZhbHVlLCB0cnVlKTtcclxuXHR9XHJcblxyXG5cclxuXHRmdW5jdGlvbiBvbkNoaWxkRGVsZXRlKGNoaWxkKXtcclxuXHRcdHZhciBrZXkgPSBjaGlsZC5uYW1lO1xyXG5cclxuXHRcdGlmKHR5cGUgPT0gJ2FycmF5Jyl7XHJcblx0XHRcdHZhbHVlLnNwbGljZShrZXksIDEpO1xyXG5cdFx0fVxyXG5cdFx0ZWxzZXtcclxuXHRcdFx0ZGVsZXRlIHZhbHVlW2tleV07XHJcblx0XHR9XHJcblxyXG5cdFx0cmVmcmVzaCgpO1xyXG5cdH1cclxuXHJcblxyXG5cdGZ1bmN0aW9uIGFkZERvbUV2ZW50TGlzdGVuZXIoZWxlbWVudCwgbmFtZSwgZm4pe1xyXG5cdFx0ZWxlbWVudC5hZGRFdmVudExpc3RlbmVyKG5hbWUsIGZuKTtcclxuXHRcdGRvbUV2ZW50TGlzdGVuZXJzLnB1c2goe2VsZW1lbnQgOiBlbGVtZW50LCBuYW1lIDogbmFtZSwgZm4gOiBmbn0pO1xyXG5cdH1cclxufSJdfQ== diff --git a/samples/wallet-mock/public/stylesheets/devtools.css b/samples/wallet-mock/public/stylesheets/devtools.css index 718ce78..fc9d496 100644 --- a/samples/wallet-mock/public/stylesheets/devtools.css +++ b/samples/wallet-mock/public/stylesheets/devtools.css @@ -122,4 +122,4 @@ .jsonView>.insert:hover{ color: rgb(0, 0, 0); background: rgb(220, 220, 220); -} \ No newline at end of file +} diff --git a/samples/wallet-mock/public/stylesheets/style.css b/samples/wallet-mock/public/stylesheets/style.css index f56dc95..62e2131 100644 --- a/samples/wallet-mock/public/stylesheets/style.css +++ b/samples/wallet-mock/public/stylesheets/style.css @@ -1,14 +1,14 @@ body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; } a { - color: #00B7FF; + color: #00B7FF; text-decoration: none; } .BigText { font-size: large; -} \ No newline at end of file +} diff --git a/samples/wallet-mock/views/index.pug b/samples/wallet-mock/views/index.pug index 6cf88bf..87f6282 100644 --- a/samples/wallet-mock/views/index.pug +++ b/samples/wallet-mock/views/index.pug @@ -23,4 +23,4 @@ block content for vc of vc_list a(href="/vc/" + vc.jti) #{vc.jti} (issuedAt: #{vc.vc.issuanceDate}) br - br \ No newline at end of file + br diff --git a/samples/wallet-mock/views/presentations.pug b/samples/wallet-mock/views/presentations.pug index 98abf4e..71b8ad1 100644 --- a/samples/wallet-mock/views/presentations.pug +++ b/samples/wallet-mock/views/presentations.pug @@ -18,4 +18,4 @@ block content for vp of vp_list a(href="/vp/" + vp.jti) #{vp.jti} (issuedAt: #{vp.vp.issuanceDate}) br - br \ No newline at end of file + br diff --git a/samples/wallet-mock/views/select-vc.pug b/samples/wallet-mock/views/select-vc.pug index f8ddf6d..82e32bc 100644 --- a/samples/wallet-mock/views/select-vc.pug +++ b/samples/wallet-mock/views/select-vc.pug @@ -15,4 +15,4 @@ block content br br br - button(type='submit') Send \ No newline at end of file + button(type='submit') Send diff --git a/samples/wallet-mock/views/vc.pug b/samples/wallet-mock/views/vc.pug index 8afc635..264877e 100644 --- a/samples/wallet-mock/views/vc.pug +++ b/samples/wallet-mock/views/vc.pug @@ -4,4 +4,4 @@ block content //- div #{vc_list} input(type='hidden' id="vc" value=vc) link(rel="stylesheet" href="/stylesheets/devtools.css") - script(src="/javascripts/index.js" defer) \ No newline at end of file + script(src="/javascripts/index.js" defer) diff --git a/src/app.ts b/src/app.ts index f7fe265..52ee73e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,7 +10,7 @@ import { communicationHandlerRouter } from './routers/communicationHandler.route import { storageRouter } from './routers/storage.router'; import { legalPersonRouter } from './routers/legal_person.router'; import verifiersRouter from './routers/verifiers.router'; -import { reviverTaggedBase64UrlToBuffer } from './util/util'; +import { replacerBufferToTaggedBase64Url, reviverTaggedBase64UrlToBuffer } from './util/util'; import * as WebSocket from 'ws'; import http from 'http'; import { appContainer } from './services/inversify.config'; @@ -27,31 +27,22 @@ const app: Express = express(); app.use(cookieParser()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json({ reviver: reviverTaggedBase64UrlToBuffer })); +app.set('json replacer', replacerBufferToTaggedBase64Url); app.use(express.static('public')); // __dirname is "/path/to/dist/src" // public is located at "/path/to/dist/src" -app.use(cors({ credentials: true, origin: true })); +app.use(cors({ + credentials: true, + origin: true, + allowedHeaders: ['Authorization', 'Content-Type', 'X-Private-Data-If-Match'], + exposedHeaders: ['X-Private-Data-ETag'], +})); // define routes and middleware here app.use('/status', statusRouter); app.use('/user', userController); -// app.get('/jwks', async (req, res) => { -// const users = await getAllUsers(); -// if (users.err) { -// return res.status(500).send({}); -// } - -// const jwksPromises = users.unwrap().map(async (user) => { -// const keys = JSON.parse(user.keys); -// const w = await NaturalPersonWallet.initializeWallet(keys); -// const did = w.key.did -// return { ...w.getPublicKey(), kid: did }; -// }) -// const jwks = await Promise.all(jwksPromises); -// return res.send(jwks); -// }) @@ -73,4 +64,3 @@ appContainer.get(TYPES.SocketManagerService).regi server.listen(config.port, () => { console.log(`Wallet Backend Server listening with ${config.url}`) }); - diff --git a/src/dto/issuance.dto.ts b/src/dto/issuance.dto.ts index 2adc353..3ded0f7 100644 --- a/src/dto/issuance.dto.ts +++ b/src/dto/issuance.dto.ts @@ -1,4 +1,4 @@ export type ConstructProofRequestDTO = { issuerUrl: string; c_nonce: string; -} \ No newline at end of file +} diff --git a/src/dto/user.dto.ts b/src/dto/user.dto.ts index dfa029b..e4d7b22 100644 --- a/src/dto/user.dto.ts +++ b/src/dto/user.dto.ts @@ -22,4 +22,4 @@ export type LoginUserRequestDTO = { export type LoginUserResponseDTO = { error?: FetchUserErrors -} \ No newline at end of file +} diff --git a/src/dto/verification.dto.ts b/src/dto/verification.dto.ts index 237d5e0..4985997 100644 --- a/src/dto/verification.dto.ts +++ b/src/dto/verification.dto.ts @@ -5,4 +5,4 @@ export type VerifyVpRequestDTO = { export type VerifyVpResponseDTO = { verificationResult: boolean; -} \ No newline at end of file +} diff --git a/src/entities/FcmToken.entity.ts b/src/entities/FcmToken.entity.ts index 3bdd4e4..c043600 100644 --- a/src/entities/FcmToken.entity.ts +++ b/src/entities/FcmToken.entity.ts @@ -1,5 +1,5 @@ -import { Column, Entity, EntityManager, ManyToOne, PrimaryGeneratedColumn, Repository } from "typeorm"; -import { UserEntity } from "./user.entity"; +import { Column, Entity, EntityManager, Equal, ManyToOne, PrimaryGeneratedColumn, Repository } from "typeorm"; +import { UserEntity, UserId } from "./user.entity"; import AppDataSource from "../AppDataSource"; import { Err, Ok, Result } from "ts-results"; @@ -10,7 +10,7 @@ export class FcmTokenEntity { @Column({ name: "value", type: "varchar", nullable: false }) value: string; - + @ManyToOne(() => UserEntity, (user) => user.fcmTokenList) user: UserEntity; } @@ -20,10 +20,10 @@ const fcmTokenRepository: Repository = AppDataSource.getReposito enum DeleteFcmTokenErr { DB_ERR = "DB_ERR" } -async function deleteAllFcmTokensForUser(did: string, options?: { entityManager?: EntityManager }): Promise> { +async function deleteAllFcmTokensForUser(id: UserId, options?: { entityManager?: EntityManager }): Promise> { try { return await (options?.entityManager || fcmTokenRepository.manager).transaction(async (manager) => { - const tokens = await manager.find(FcmTokenEntity, { where: { user: { did: did } } }); + const tokens = await manager.find(FcmTokenEntity, { where: { user: { uuid: Equal(id) } } }); await manager.remove(tokens); return Ok({}); }); @@ -36,4 +36,4 @@ async function deleteAllFcmTokensForUser(did: string, options?: { entityManager? export { deleteAllFcmTokensForUser -} \ No newline at end of file +} diff --git a/src/entities/LegalPerson.entity.ts b/src/entities/LegalPerson.entity.ts index 3514224..fab1f81 100644 --- a/src/entities/LegalPerson.entity.ts +++ b/src/entities/LegalPerson.entity.ts @@ -6,8 +6,8 @@ import AppDataSource from "../AppDataSource"; @Entity({ name: "legal_person" }) class LegalPersonEntity { - @PrimaryGeneratedColumn() - id: number = -1; + @PrimaryGeneratedColumn() + id: number = -1; @Column({ nullable: false }) @@ -74,7 +74,7 @@ async function createIssuer(createIssuer: CreateLegalPerson) { async function getAllLegalPersons(): Promise> { try { - const lps = await legalPersonRepository + const lps = await legalPersonRepository .createQueryBuilder("legal_person") .select(["legal_person.id", "legal_person.friendlyName", "legal_person.url", "legal_person.did"]) .getMany(); @@ -88,7 +88,7 @@ async function getAllLegalPersons(): Promise> { try { - const vcList = await legalPersonRepository + const vcList = await legalPersonRepository .createQueryBuilder("legal_person") .getMany(); @@ -106,7 +106,7 @@ async function getAllLegalPersonsDIDs(): Promise> { try { - const issuersList = await legalPersonRepository + const issuersList = await legalPersonRepository .createQueryBuilder("legal_person") .select(["legal_person.id", "legal_person.friendlyName", "legal_person.url", "legal_person.did"]) .where("friendlyName LIKE '%:friendlyNameSubstring%", { friendlyNameSubstring }) @@ -125,10 +125,10 @@ async function getLegalPersonsBySearchParams(friendlyNameSubstring: string): Pro } /** - * Will also update the issuer DB entity with the latest metadata - * @param id - * @returns - */ +* Will also update the issuer DB entity with the latest metadata +* @param id +* @returns +*/ async function getLegalPersonById(id: number): Promise> { try { @@ -146,10 +146,10 @@ async function getLegalPersonById(id: number): Promise> { try { @@ -168,10 +168,10 @@ async function getLegalPersonByDID(did: string): Promise> { try { diff --git a/src/entities/VerifiableCredential.entity.ts b/src/entities/VerifiableCredential.entity.ts index 515eb28..6fd9dc9 100644 --- a/src/entities/VerifiableCredential.entity.ts +++ b/src/entities/VerifiableCredential.entity.ts @@ -200,4 +200,4 @@ export { deleteVerifiableCredential, getVerifiableCredentialByCredentialIdentifier, deleteAllCredentialsWithHolderDID -} \ No newline at end of file +} diff --git a/src/entities/VerifiablePresentation.entity.ts b/src/entities/VerifiablePresentation.entity.ts index 11d25c9..1b29e32 100644 --- a/src/entities/VerifiablePresentation.entity.ts +++ b/src/entities/VerifiablePresentation.entity.ts @@ -36,7 +36,7 @@ export class VerifiablePresentationEntity { // @Column({ enum: PresentationTypes, type: 'enum', nullable: false }) // format: PresentationTypes | null = null; // = PresentationTypes.JWT_VP; // 'ldp_vp' or 'jwt_vp' - + @Column({ type: "datetime", nullable: false }) issuanceDate: Date = new Date(); } @@ -131,7 +131,7 @@ async function deletePresentationsByCredentialId(holderDID:string, credentialIde async function getAllVerifiablePresentations(holderDID: string): Promise> { try { - const vpList = await verifiablePresentationRepository + const vpList = await verifiablePresentationRepository .createQueryBuilder("vp") .where("vp.holderDID = :did", { did: holderDID }) .getMany(); @@ -158,7 +158,7 @@ async function getAllVerifiablePresentations(holderDID: string): Promise> { try { - const vp = await verifiablePresentationRepository + const vp = await verifiablePresentationRepository .createQueryBuilder("vp") .where("vp.presentationIdentifier = :presentationIdentifier and vp.holderDID = :holderDID", { holderDID, presentationIdentifier }) .getOne(); @@ -204,4 +204,4 @@ export { deletePresentationsByCredentialId, getPresentationByIdentifier, deleteAllPresentationsWithHolderDID -} \ No newline at end of file +} diff --git a/src/entities/WebauthnChallenge.entity.ts b/src/entities/WebauthnChallenge.entity.ts index 01be7ca..eb29923 100644 --- a/src/entities/WebauthnChallenge.entity.ts +++ b/src/entities/WebauthnChallenge.entity.ts @@ -2,6 +2,7 @@ import { Err, Ok, Result } from "ts-results"; import { Entity, PrimaryColumn, Column, Repository} from "typeorm" import AppDataSource from "../AppDataSource"; import crypto from "node:crypto"; +import { UserId } from "./user.entity"; @Entity({ name: "webauthn_challenge" }) class WebauthnChallengeEntity { @PrimaryColumn() @@ -10,9 +11,20 @@ class WebauthnChallengeEntity { @Column({ nullable: false}) type: string; - // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 - @Column({ nullable: true, default: () => "NULL" }) - userHandle?: string; + /** + * This was renamed in PR (TBD). + * We keep the old database column name for forward- and backwards compatibility between application and schema versions. + */ + @Column({ + name: "userHandle", + type: "varchar", + length: 255, + nullable: true, + // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 + default: () => "NULL", + transformer: { from: (id) => id && UserId.fromId(id), to: (userId: UserId) => userId?.id }, + }) + userId?: UserId; @Column({ nullable: false }) challenge: Buffer; @@ -29,7 +41,7 @@ const TIMEOUT_MILLISECONDS = 15 * 60 * 1000; type CreatedChallenge = { id: string; - userHandle?: string; + userId?: UserId; challenge: Buffer; prfSalt?: Buffer; } @@ -43,10 +55,10 @@ enum ChallengeErr { const challengeRepository: Repository = AppDataSource.getRepository(WebauthnChallengeEntity); -async function createChallenge(type: "create" | "get", userHandle?: string, prfSalt?: Buffer): Promise> { +async function createChallenge(type: "create" | "get", userId?: UserId, prfSalt?: Buffer): Promise> { try { const returnData = { - userHandle, + userId, prfSalt, id: crypto.randomUUID(), challenge: crypto.randomBytes(32), diff --git a/src/entities/common.entity.ts b/src/entities/common.entity.ts index 9d57d8b..378301e 100644 --- a/src/entities/common.entity.ts +++ b/src/entities/common.entity.ts @@ -2,6 +2,7 @@ import { Result } from "ts-results"; import { EntityManager } from "typeorm" import AppDataSource from "../AppDataSource"; +import { isResult } from "../util/util"; /** * Run the provided callback in a database transaction. The `entityManager` can @@ -17,7 +18,7 @@ import AppDataSource from "../AppDataSource"; export async function runTransaction(runInTransaction: (entityManager: EntityManager) => Promise | T>): Promise { return await AppDataSource.manager.transaction(async (entityManager) => { const result = await runInTransaction(entityManager); - if ("val" in result && "ok" in result && "err" in result) { + if (isResult(result)) { if (result.ok) { return Promise.resolve(result.val); } else { diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 768a7b6..ed20efc 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,22 +1,88 @@ import { Err, Ok, Result } from "ts-results"; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, Repository, Generated, EntityManager, DeepPartial, JoinColumn } from "typeorm" +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, Repository, EntityManager, DeepPartial, Generated, Equal } from "typeorm" import crypto from "node:crypto"; import base64url from "base64url"; +import * as uuid from 'uuid'; import AppDataSource from "../AppDataSource"; import * as scrypt from "../scrypt"; import { FcmTokenEntity } from "./FcmToken.entity"; +import { checkedUpdate, EtagUpdate, isResult } from "../util/util"; +import { runTransaction } from "./common.entity"; export enum WalletType { DB, CLIENT } + +/** + * Compute a value suitable to use as an ETag-style HTTP header for the private data field. + */ +export function privateDataEtag(privateData: Buffer): string { + const etag = base64url.toBase64(base64url.encode(crypto.createHash('sha256').update(privateData).digest())); + return `"${etag}"`; +} + + +// Duplicated in wallet-frontend +export class UserId { + public readonly id: string; + private constructor(id: string) { + this.id = id; + } + + public toString(): string { + return `UserId(this.id)`; + } + + public toJSON(): string { + return this.id; + } + + static generate(): UserId { + return new UserId(uuid.v4()); + } + + static fromId(id: string): UserId { + return new UserId(id); + } + + static fromUserHandle(userHandle: Buffer): UserId { + return new UserId(userHandle.toString()); + } + + public asUserHandle(): Buffer { + return Buffer.from(this.id, "utf8"); + } +} + + @Entity({ name: "user" }) class UserEntity { - @PrimaryGeneratedColumn() - id: number; - + /** + * This was obsoleted by PR (TBD). + * We keep the table column for forward- and backwards compatibility between application and schema versions. + * It still needs to be the primary ID in order for table relations to continue working. + */ + @Column({ primary: true, unique: true, nullable: false, update: false }) + @Generated("increment") + private id: number; + + /** + * This was renamed in PR (TBD). + * We keep the old database column name for forward- and backwards compatibility between application and schema versions. + */ + @Column({ + unique: true, + nullable: false, + update: false, + name: "webauthnUserHandle", + type: "varchar", + length: 36, + transformer: { from: UserId.fromId, to: (userId: UserId) => userId.id }, + }) + uuid: UserId; // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 @Column({ unique: true, nullable: true, default: () => "NULL" }) @@ -44,10 +110,6 @@ class UserEntity { @Column({ type: "blob", nullable: false }) privateData: Buffer; - @Column({ nullable: false }) - @Generated("uuid") - webauthnUserHandle: string; - @Column({ type: "enum" ,enum: WalletType, default: WalletType.DB }) walletType: WalletType; @@ -70,23 +132,27 @@ class WebauthnCredentialEntity { @ManyToOne(() => UserEntity, (user) => user.webauthnCredentials, { nullable: false }) user: UserEntity; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) credentialId: Buffer; - @Column({ nullable: false }) - userHandle: Buffer; + /** + * This was obsoleted by PR (TBD). + * We keep the table column for forward- and backwards compatibility between application and schema versions. + */ + @Column({ name: "userHandle", nullable: false, select: false, update: false }) + _userHandle: Buffer; // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 @Column({ nullable: true, default: () => "NULL" }) nickname: string; - @Column({ type: "datetime", nullable: false }) + @Column({ type: "datetime", nullable: false, update: false }) createTime: Date; @Column({ type: "datetime", nullable: false }) lastUseTime: Date; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) publicKeyCose: Buffer; @Column({ nullable: false }) @@ -95,10 +161,10 @@ class WebauthnCredentialEntity { @Column("simple-json", { nullable: false }) transports: string[]; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) attestationObject: Buffer; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) create_clientDataJSON: Buffer; @Column({ nullable: false }) @@ -117,18 +183,15 @@ class WebauthnCredentialEntity { type CreateUser = { username: string; displayName: string, - did: string; passwordHash: string; fcmToken: string; privateData: Buffer; - webauthnUserHandle: string; } | { + uuid: UserId; displayName: string, - did: string; keys: Buffer; fcmToken: string; privateData: Buffer; - webauthnUserHandle: string; webauthnCredentials: WebauthnCredentialEntity[]; } @@ -146,6 +209,7 @@ enum UpdateUserErr { NOT_EXISTS = "NOT_EXISTS", DB_ERR = "DB_ERR", LAST_WEBAUTHN_CREDENTIAL = "LAST_WEBAUTHN_CREDENTIAL", + PRIVATE_DATA_CONFLICT = "PRIVATE_DATA_CONFLICT", } enum UpdateFcmError { @@ -163,8 +227,11 @@ const webauthnCredentialRepository: Repository = AppDa async function createUser(createUser: CreateUser, isAdmin: boolean = false): Promise> { try { + const uuid = "uuid" in createUser ? createUser.uuid : UserId.generate(); const user = await userRepository.save(userRepository.create({ ...createUser, + uuid, + did: uuid.id, isAdmin, })); const fcmTokenEntity = new FcmTokenEntity(); @@ -180,30 +247,9 @@ async function createUser(createUser: CreateUser, isAdmin: boolean = false): Pro } } -async function storeKeypair(username: string, did: string, keys: Buffer): Promise> { - try { - const res = await AppDataSource - .createQueryBuilder() - .update(UserEntity) - .set({ keys: keys, did: did }) - .where('username = :username', { username }) - .execute(); - - return Ok({}); - } - catch(e) { - console.log(e); - return Err(e); - } -} - -async function getUserByDID(did: string): Promise> { +async function getUser(id: UserId): Promise> { try { - const res = await userRepository.findOne({ - where: { - did: did - } - }); + const res = await userRepository.findOne({ where: { uuid: Equal(id) } }); if (!res) { return Err(GetUserErr.NOT_EXISTS); } @@ -215,19 +261,12 @@ async function getUserByDID(did: string): Promise } } -async function deleteUserByDID(did: string, options?: { entityManager: EntityManager }): Promise> { +async function deleteUser(id: UserId, options?: { entityManager: EntityManager }): Promise> { try { return await (options?.entityManager || userRepository.manager).transaction(async (manager) => { - const userRes = await manager.findOne(UserEntity, { where: { did: did }}); - - await manager.delete(WebauthnCredentialEntity, { - user: { id: userRes.id } - }); - - await manager.delete(UserEntity, { - did: did - }); - + const user = await manager.findOne(UserEntity, { where: { uuid: Equal(id) }}); + await manager.delete(WebauthnCredentialEntity, { user }); + await manager.delete(UserEntity, { uuid: id }); return Ok({}) }); } @@ -278,12 +317,12 @@ async function getUserByCredentials(username: string, password: string): Promise } -async function getUserByWebauthnCredential(userHandle: string, credentialId: Buffer): Promise> { +async function getUserByWebauthnCredential(userId: UserId, credentialId: Buffer): Promise> { try { - console.log("getUserByWebauthnCredential", userHandle, base64url.encode(credentialId)); + console.log("getUserByWebauthnCredential", userId, base64url.encode(credentialId)); const q = userRepository.createQueryBuilder("user") .leftJoinAndSelect("user.webauthnCredentials", "credential") - .where("user.webauthnUserHandle = :userHandle", { userHandle }) + .where("user.uuid = :uuid", { uuid: userId.id }) .andWhere("credential.credentialId = :credentialId", { credentialId }); console.log(q.getSql()); const userRes = await q.getOne(); @@ -319,25 +358,6 @@ async function getAllUsers(): Promise> { return Err(GetUserErr.DB_ERR) } } -// async function addFcmTokenByDID(did: string, newFcmToken: string) { -// try { -// const res = await AppDataSource.getRepository(UserEntity) -// .createQueryBuilder("user") -// .where("user.did = :did", { did: did }) -// .getOne(); -// const fcmTokens: string[] = JSON.parse(res.fcmTokens.toString()); -// fcmTokens.push(newFcmToken); -// const updateRes = await AppDataSource.getRepository(UserEntity) -// .createQueryBuilder("user") -// .update({ fcmTokens: JSON.stringify(fcmTokens) }) -// .where("did = :did", { did: did }) -// .execute(); -// } -// catch(err) { -// console.log(err); -// return Err(UpdateFcmError.DB_ERR); -// } -// } function newWebauthnCredentialEntity(data: DeepPartial, manager?: EntityManager): WebauthnCredentialEntity { const entity = (manager || webauthnCredentialRepository.manager).create(WebauthnCredentialEntity, data); @@ -346,27 +366,37 @@ function newWebauthnCredentialEntity(data: DeepPartial return entity; } -async function updateUserByDID(did: string, update: (user: UserEntity, entityManager: EntityManager) => UserEntity): Promise> { - return await userRepository.manager.transaction(async (manager) => { - const res = await manager.findOne(UserEntity, { - where: { - did: did +async function updateUser(id: UserId, update: (user: UserEntity, entityManager: EntityManager) => UserEntity | Result): Promise> { + try { + return await userRepository.manager.transaction(async (manager) => { + const res = await manager.findOne(UserEntity, { where: { uuid: Equal(id) } }); + if (!res) { + return Promise.reject(Err(UpdateUserErr.NOT_EXISTS)); } - }); - if (!res) { - return Err(UpdateUserErr.NOT_EXISTS); - } - - const updatedUser = update(res, manager); - try { - await manager.save(updatedUser); - return Ok(res); - } catch (e) { + const updatedUser = update(res, manager); + if (isResult(updatedUser)) { + if (updatedUser.ok) { + await manager.save(updatedUser.val); + return updatedUser; + } else { + return updatedUser; + } + } else { + await manager.save(updatedUser); + return Ok(updatedUser); + } + }); + } catch (e) { + if (isResult(e)) { + if (e.err) { + return e as Result; + } + } else { console.log(e); return Err(UpdateUserErr.DB_ERR); } - }); + } } async function updateWebauthnCredentialWithManager( @@ -393,12 +423,12 @@ async function updateWebauthnCredential( }); } -async function updateWebauthnCredentialById(userDid: string, credentialUuid: string, update: (credential: WebauthnCredentialEntity, manager: EntityManager) => WebauthnCredentialEntity): Promise> { - console.log("updateWebauthnCredentialById", userDid, credentialUuid); +async function updateWebauthnCredentialById(userId: UserId, credentialUuid: string, update: (credential: WebauthnCredentialEntity, manager: EntityManager) => WebauthnCredentialEntity): Promise> { + console.log("updateWebauthnCredentialById", userId, credentialUuid); return await webauthnCredentialRepository.manager.transaction(async (manager) => { const q = userRepository.createQueryBuilder("user") .leftJoinAndSelect("user.webauthnCredentials", "credential") - .where("user.did = :userDid", { userDid }) + .where("user.uuid = :uuid", { uuid: userId.id }) .andWhere("credential.id = :credentialUuid", { credentialUuid }); console.log("q", q.getQueryAndParameters()); const userRes = await q.getOne(); @@ -408,11 +438,10 @@ async function updateWebauthnCredentialById(userDid: string, credentialUuid: str }); } -async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, newPrivateData: Buffer): Promise> { +async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, updatePrivateData: EtagUpdate): Promise> { try { - - return await userRepository.manager.transaction(async (manager) => { - const userRes = await manager.findOne(UserEntity, { where: { did: user.did }}); + return Ok(await runTransaction(async (manager) => { + const userRes = await manager.findOne(UserEntity, { where: { uuid: Equal(user.uuid) }}); if (!userRes) { return Err(UpdateUserErr.NOT_EXISTS); } @@ -432,16 +461,27 @@ async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string .where({ user, id: credentialUuid }) .execute(); if (res.affected > 0) { - await manager.update(UserEntity, { did: user.did }, { privateData: newPrivateData }); - return Ok({}); + const newPrivateData = checkedUpdate( + updatePrivateData.expectTag, + privateDataEtag, + { + currentValue: userRes.privateData, + newValue: updatePrivateData.newValue, + }); + if (newPrivateData.ok) { + await manager.update(UserEntity, { uuid: user.uuid }, { privateData: newPrivateData.val }); + return Ok.EMPTY; + } else { + return Err(UpdateUserErr.PRIVATE_DATA_CONFLICT); + } } else if (res.affected === 0) { return Err(UpdateUserErr.NOT_EXISTS); } - }); + })); } catch (e) { - console.log(e); - return Err(UpdateUserErr.DB_ERR); + console.log('Failed to delete WebAuthn credential:', e); + return Err(e); } } @@ -452,15 +492,15 @@ export { GetUserErr, UpdateUserErr, createUser, - getUserByDID, + getUser, getUserByCredentials, UpdateFcmError, getUserByWebauthnCredential, getAllUsers, newWebauthnCredentialEntity, - updateUserByDID, + updateUser, deleteWebauthnCredential, updateWebauthnCredential, updateWebauthnCredentialById, - deleteUserByDID + deleteUser, } diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 308ae69..074f4e1 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -31,6 +31,7 @@ const sendPushNotification = async (fcm_token, title, body) => { body, }, data: { + scope: '/notifications/' }, apns: { payload: { @@ -62,4 +63,4 @@ const sendPushNotification = async (fcm_token, title, body) => { export { sendPushNotification -} \ No newline at end of file +} diff --git a/src/lib/leafnodepaths.ts b/src/lib/leafnodepaths.ts index 746051f..785f7a6 100644 --- a/src/lib/leafnodepaths.ts +++ b/src/lib/leafnodepaths.ts @@ -1,15 +1,15 @@ import { JSONPath } from "jsonpath-plus"; export function getLeafNodesWithPath(verifiableCredential, obj, path = "$.credentialSubject.") { - // Array to store leaf nodes with paths - let leafNodesWithPath = []; + // Array to store leaf nodes with paths + let leafNodesWithPath = []; + + // Recursive function to traverse the object + function traverse(obj, currentPath) { + for (let key in obj) { + const newPath = currentPath !== "$.credentialSubject." ? `${currentPath}.${key}` : `${currentPath}${key}`; - // Recursive function to traverse the object - function traverse(obj, currentPath) { - for (let key in obj) { - const newPath = currentPath !== "$.credentialSubject." ? `${currentPath}.${key}` : `${currentPath}${key}`; - // Add leaf node with path to the array if (Object.keys(obj[key]).length === 1 && !(obj[key] instanceof Array) && obj[key].display) { console.log("Path = ", newPath) @@ -18,17 +18,17 @@ export function getLeafNodesWithPath(verifiableCredential, obj, path = "$.creden leafNodesWithPath.push({ key: key, path: newPath, friendlyName: obj[key].display[0].name, value: valueFoundInVC }); } else if (typeof obj[key] === "object" && obj[key] !== null) { - // Recursively traverse nested objects - traverse(obj[key], newPath); - } - - } - } + // Recursively traverse nested objects + traverse(obj[key], newPath); + } + + } + } - // Start traversing the object - traverse(obj, path); + // Start traversing the object + traverse(obj, path); console.log("Leafnode paths = ", leafNodesWithPath) - // Group leaf nodes by path + // Group leaf nodes by path return leafNodesWithPath -} \ No newline at end of file +} diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 24f84a0..624e163 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -1,78 +1,86 @@ import { Request, Response, NextFunction } from "express"; -import { jwtVerify } from 'jose'; +import { jwtVerify, SignJWT } from 'jose'; import config from "../../config"; -import { getUserByDID } from "../entities/user.entity"; +import { getUser, UserEntity, UserId } from "../entities/user.entity"; + + +type TokenPayloadVersion = 1; +const TOKEN_PAYLOAD_VERSION: TokenPayloadVersion = 1; + +type AppTokenPayload = { + // Increment TokenPayloadVersion whenever AppTokenPayload content changes to invalidate existing tokens + v: TokenPayloadVersion; + uuid: string; +} export type AppTokenUser = { username: string; + id: UserId; did: string; } -function getCookieDictionary(cookies: any) { - const cookieList = cookies.split('; '); - let cookieDict: any = {}; - for (const cookie of cookieList) { - const key = cookie.split('=')[0] as string; - - const val = cookie.split('=')[1]; - cookieDict[key] = val; - - } - return cookieDict; +export async function createAppToken(user: UserEntity): Promise { + const secret = new TextEncoder().encode(config.appSecret); + const payload: AppTokenPayload = { + v: TOKEN_PAYLOAD_VERSION, + uuid: user.uuid.id, + }; + return await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .sign(secret); } -async function verifyApptoken(jwt: string): Promise<{valid: boolean, payload: any}> { +async function verifyApptoken(jwt: string): Promise { const secret = new TextEncoder().encode(config.appSecret); - try { - const { payload, protectedHeader } = await jwtVerify(jwt, secret); - return { valid: true, payload: payload }; - } - catch (err) { - console.log('Signature verification failed'); - return { valid: false, payload: {}} - } + try { + const { payload, protectedHeader } = await jwtVerify(jwt, secret); + if (payload?.v === TOKEN_PAYLOAD_VERSION) { + // The combination of a valid signature and the correct version + // guarantees that this type assertion is sound + return payload as AppTokenPayload; + } else { + console.log(`Incorrect token version: expected: ${TOKEN_PAYLOAD_VERSION}, got: ${payload?.v}`); + return null; + } + } + catch (err) { + console.log('Signature verification failed'); + return false; + } } export function AuthMiddleware(req: Request, res: Response, next: NextFunction) { - let token: string; - const authorizationHeader = req.headers.authorization; + const authorizationHeader = req.headers?.authorization; console.log("Authorization header = ", authorizationHeader) - if (req.headers != undefined && authorizationHeader != undefined) { - if (authorizationHeader.split(' ')[0] !== 'Bearer') { - res.status(401).send(); - return; - } - token = authorizationHeader.split(' ')[1]; - } - else { - console.log("Unauthorized access to token: ", authorizationHeader?.split(' ')[1]); - res.status(401).send(); // Unauthorized - return; - } + if (authorizationHeader?.substring(0, 7) !== 'Bearer ') { + console.log("Invalid authorization header:", authorizationHeader); + res.status(401).send(); + return; + } - verifyApptoken(token).then(async ({valid, payload}) => { - if (valid === false) { + let token: string = authorizationHeader.substring(7); + + verifyApptoken(token).then(async (payload) => { + if (!payload) { console.log("Unauthorized access to ", token); res.status(401).send(); // Unauthorized return; } - // success - req.user = { - username: "", - did: "" - } as AppTokenUser; - req.user.did = (payload as AppTokenUser).did; - const userRes = await getUserByDID(req.user.did); - if (userRes.err) { - res.status(401).send(); // Unauthorized - return; + const userId = UserId.fromId(payload.uuid); + const userRes = await getUser(userId); + if (userRes.ok) { + req.user = { + username: userRes.val.username, + id: userId, + did: userRes.val.did, + }; + return next(); } - const user = userRes.unwrap(); - req.user.username = user.username; - req.user.did = user.did; - return next(); + + res.status(401).send(); // Unauthorized + return; }) .catch(e => { console.log("Unauthorized access to ", token); diff --git a/src/routers/communicationHandler.router.ts b/src/routers/communicationHandler.router.ts index 1e268c9..a356d54 100644 --- a/src/routers/communicationHandler.router.ts +++ b/src/routers/communicationHandler.router.ts @@ -3,7 +3,7 @@ import express, { Router } from 'express'; import { AuthMiddleware } from '../middlewares/auth.middleware'; import _ from 'lodash'; import { appContainer } from '../services/inversify.config'; -import { HandleOutboundRequestError, IssuanceErr, OpenidCredentialReceiving, OutboundCommunication, SendResponseError } from '../services/interfaces'; +import { HandleOutboundRequestError, OpenidCredentialReceiving, OutboundCommunication, SendResponseError } from '../services/interfaces'; import { TYPES } from '../services/types'; import * as z from 'zod'; @@ -44,7 +44,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { if (generateAuthorizationRequestSchemaResult.success) { try { const { legal_person_did } = req.body; - const result = await openidForCredentialIssuanceService.generateAuthorizationRequestURL(req.user.did, null, legal_person_did); + const result = await openidForCredentialIssuanceService.generateAuthorizationRequestURL(req.user.id, null, legal_person_did); console.log("Succesfully handled by generateAuthorizationRequestURL"); return res.send(result); } @@ -57,7 +57,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { url, } = req.body; - const result = await openidForCredentialIssuanceService.generateAuthorizationRequestURL(req.user.did, url, null); + const result = await openidForCredentialIssuanceService.generateAuthorizationRequestURL(req.user.id, url, null); console.log("Successfully handled by generateAuthorizationRequestURL"); return res.send(result); } @@ -70,11 +70,11 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { url, } = req.body; - + if (!(new URL(url).searchParams.get("code"))) { throw new Error("No code was provided"); } - const result = await openidForCredentialIssuanceService.handleAuthorizationResponse(req.user.did, url); + const result = await openidForCredentialIssuanceService.handleAuthorizationResponse(req.user.id, url); if (result.ok) { console.log("Successfully handled by handleAuthorizationResponse"); return res.send({}); @@ -89,8 +89,8 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { user_pin } = req.body; - - const response = await openidForCredentialIssuanceService.requestCredentialsWithPreAuthorizedGrant(req.user.did, user_pin); + + const response = await openidForCredentialIssuanceService.requestCredentialsWithPreAuthorizedGrant(req.user.id, user_pin); console.log("Response = ", response) if (response.error) { return res.status(401).send({ error: response.error }); @@ -105,7 +105,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { if (handleSIOPRequestResult.success) { const { url, camera_was_used } = handleSIOPRequestResult.data; try { - const outboundRequestResult = await openidForPresentationService.handleRequest(req.user.did, url, camera_was_used); + const outboundRequestResult = await openidForPresentationService.handleRequest(req.user.id, url, camera_was_used); if (!outboundRequestResult.ok) { if (outboundRequestResult.val == HandleOutboundRequestError.INSUFFICIENT_CREDENTIALS) { return res.send({ error: HandleOutboundRequestError.INSUFFICIENT_CREDENTIALS }); @@ -139,17 +139,17 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { verifiable_credentials_map, // { "descriptor_id1": "urn:vid:123", "descriptor_id1": "urn:vid:645" } } = req.body; - + console.log("Credentials map = ", verifiable_credentials_map) const selection = new Map(Object.entries(verifiable_credentials_map)) as Map; console.log("Selection = ", verifiable_credentials_map) try { - const result = await openidForPresentationService.sendResponse(req.user.did, selection); - + const result = await openidForPresentationService.sendResponse(req.user.id, selection); + if (!result.ok) { return res.send({ error: SendResponseError.SEND_RESPONSE_ERROR }); } - + const { redirect_to } = result.val; console.log("Successfully handled by sendResponse"); return res.send({ redirect_to }); @@ -157,7 +157,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { catch(error) { const errText = `Error generating authorization response: ${error}`; console.log(errText); - } + } } return res.status(400).send({ error: "Could not handle" }); }); diff --git a/src/routers/legal_person.router.ts b/src/routers/legal_person.router.ts index 2181d93..e62dbcb 100644 --- a/src/routers/legal_person.router.ts +++ b/src/routers/legal_person.router.ts @@ -15,4 +15,4 @@ legalPersonRouter.get('/issuers/all', async (req, res) => { } }) -export { legalPersonRouter }; \ No newline at end of file +export { legalPersonRouter }; diff --git a/src/routers/status.router.ts b/src/routers/status.router.ts index 6b1cdf7..17d0ff3 100644 --- a/src/routers/status.router.ts +++ b/src/routers/status.router.ts @@ -12,4 +12,4 @@ statusRouter.get('/', (_req: Request, res: Response) => { export { statusRouter -} \ No newline at end of file +} diff --git a/src/routers/storage.router.ts b/src/routers/storage.router.ts index 552ac88..8a1473b 100644 --- a/src/routers/storage.router.ts +++ b/src/routers/storage.router.ts @@ -1,4 +1,4 @@ -import express, { Router } from "express"; +import express, { Request, Response, Router } from "express"; import { getAllVerifiableCredentials, getVerifiableCredentialByCredentialIdentifier, deleteVerifiableCredential } from "../entities/VerifiableCredential.entity"; import { getAllVerifiablePresentations, getPresentationByIdentifier } from "../entities/VerifiablePresentation.entity"; @@ -14,7 +14,7 @@ storageRouter.get('/vp', getAllVerifiablePresentationsController); storageRouter.get('/vp/:presentation_identifier', getPresentationByPresentationIdentifierController); -async function getAllVerifiableCredentialsController(req, res) { +async function getAllVerifiableCredentialsController(req: Request, res: Response) { const holderDID = req.user.did; console.log("Holder did", holderDID) const vcListResult = await getAllVerifiableCredentials(holderDID); @@ -34,8 +34,8 @@ async function getAllVerifiableCredentialsController(req, res) { } -async function getVerifiableCredentialByCredentialIdentifierController(req, res) { - const holderDID = req.user.did; +async function getVerifiableCredentialByCredentialIdentifierController(req: Request, res: Response) { + const holderDID = req.user.did; const { credential_identifier } = req.params; const vcFetchResult = await getVerifiableCredentialByCredentialIdentifier(holderDID, credential_identifier); if (vcFetchResult.err) { @@ -46,7 +46,7 @@ async function getVerifiableCredentialByCredentialIdentifierController(req, res) res.status(200).send(vc); } -async function deleteVerifiableCredentialController(req, res) { +async function deleteVerifiableCredentialController(req: Request, res: Response) { const holderDID = req.user.did; const { credential_identifier } = req.params; const deleteResult = await deleteVerifiableCredential(holderDID, credential_identifier); @@ -58,7 +58,7 @@ async function deleteVerifiableCredentialController(req, res) { -async function getAllVerifiablePresentationsController(req, res) { +async function getAllVerifiablePresentationsController(req: Request, res: Response) { const holderDID = req.user.did; const vpListResult = await getAllVerifiablePresentations(holderDID); if (vpListResult.err) { @@ -75,10 +75,10 @@ async function getAllVerifiablePresentationsController(req, res) { res.status(200).send({ vp_list: vp_list }) } -async function getPresentationByPresentationIdentifierController(req, res) { - const holderDID = req.user.did; +async function getPresentationByPresentationIdentifierController(req: Request, res: Response) { + const holderDID = req.user.did; const { presentation_identifier } = req.params; - + const vpResult = await getPresentationByIdentifier(holderDID, presentation_identifier); if (vpResult.err) { return res.status(500).send({ error: vpResult.val }) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 057c4c9..82bb1c5 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -1,5 +1,4 @@ import express, { Request, Response, Router } from 'express'; -import { SignJWT } from 'jose'; import * as uuid from 'uuid'; import crypto from 'node:crypto'; import * as SimpleWebauthn from '@simplewebauthn/server'; @@ -7,9 +6,9 @@ import base64url from 'base64url'; import { EntityManager } from "typeorm" import config from '../../config'; -import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, newWebauthnCredentialEntity, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; -import { jsonParseTaggedBinary, jsonStringifyTaggedBinary } from '../util/util'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; +import { CreateUser, createUser, deleteUser, deleteWebauthnCredential, getUserByCredentials, getUser, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUser, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity, UserId } from '../entities/user.entity'; +import { checkedUpdate, EtagUpdate, jsonParseTaggedBinary } from '../util/util'; +import { AuthMiddleware, createAppToken } from '../middlewares/auth.middleware'; import { ChallengeErr, createChallenge, popChallenge } from '../entities/WebauthnChallenge.entity'; import * as webauthn from '../webauthn'; import * as scrypt from "../scrypt"; @@ -36,21 +35,24 @@ userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{ did: string, appToken: string, username?: string, displayName: string, privateData: string }> { - const secret = new TextEncoder().encode(config.appSecret); - const appToken = await new SignJWT({ did: user.did }) - .setProtectedHeader({ alg: "HS256" }) - .sign(secret); +async function initSession(user: UserEntity): Promise<{ + uuid: UserId, + appToken: string, + username?: string, + displayName: string, + privateData: Buffer, + webauthnRpId: string, +}> { return { - appToken, - did: user.did, + uuid: user.uuid, + appToken: await createAppToken(user), displayName: user.displayName || user.username, - privateData: user.privateData.toString(), + privateData: user.privateData, username: user.username, + webauthnRpId: webauthn.getRpId(), }; } - noAuthUserController.post('/register', async (req: Request, res: Response) => { const username = req.body.username; const password = req.body.password; @@ -60,7 +62,7 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { } const walletInitializationResult = await walletKeystoreManagerService.initializeWallet( - {...req.body as RegistrationParams } + { ...req.body as RegistrationParams } ); if (walletInitializationResult.err) { @@ -72,12 +74,13 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { ...walletInitializationResult.unwrap(), username: username ? username : "", passwordHash: passwordHash, - webauthnUserHandle: uuid.v4(), }; const result = (await createUser(newUser)); if (result.ok) { - res.status(200).send(await initSession(result.val)); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(result.val.privateData) }) + .send(await initSession(result.val)); } else { console.log("Failed to create user") @@ -98,18 +101,21 @@ noAuthUserController.post('/login', async (req: Request, res: Response) => { } console.log('user res = ', userRes) const user = userRes.unwrap(); - res.status(200).send(await initSession(user)); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .send(await initSession(user)); }) noAuthUserController.post('/register/db-keys', async (req: Request, res: Response) => { }) noAuthUserController.post('/login/db-keys', async (req: Request, res: Response) => { - + }) noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: Response) => { - const challengeRes = await createChallenge("create", uuid.v4()); + const userId = UserId.generate(); + const challengeRes = await createChallenge("create", userId); if (challengeRes.err) { res.status(500).send({}); return; @@ -119,16 +125,16 @@ noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: const createOptions = webauthn.makeCreateOptions({ challenge: challenge.challenge, user: { - webauthnUserHandle: challenge.userHandle, + uuid: userId, name: "", displayName: "", }, }); - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ challengeId: challenge.id, createOptions, - })); + }); }); noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: Response) => { @@ -148,7 +154,16 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const credential = req.body.credential; const verification = await SimpleWebauthn.verifyRegistrationResponse({ - response: credential, + response: { + type: credential.type, + id: credential.id, + rawId: credential.id, // SimpleWebauthn requires this base64url encoded + response: { + attestationObject: base64url.encode(credential.response.attestationObject), + clientDataJSON: base64url.encode(credential.response.clientDataJSON), + }, + clientExtensionResults: credential.clientExtensionResults, + }, expectedChallenge: base64url.encode(challenge.challenge), expectedOrigin: config.webauthn.origin, expectedRPID: config.webauthn.rp.id, @@ -156,41 +171,42 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: }); if (verification.verified) { - const webauthnUserHandle = challenge.userHandle; - if (!webauthnUserHandle) { + if (!challenge.userId) { res.status(500).send({}); return; } const walletInitializationResult = await walletKeystoreManagerService.initializeWallet( - {...req.body as RegistrationParams } + { ...req.body as RegistrationParams } ); - + if (walletInitializationResult.err) { return res.status(400).send({ error: walletInitializationResult.val }) } const newUser: CreateUser = { ...walletInitializationResult.unwrap(), - webauthnUserHandle, + uuid: challenge.userId, webauthnCredentials: [ newWebauthnCredentialEntity({ - credentialId: Buffer.from(verification.registrationInfo.credentialID), - userHandle: Buffer.from(webauthnUserHandle), + credentialId: credential.rawId, + _userHandle: challenge.userId.asUserHandle(), nickname: req.body.nickname, publicKeyCose: Buffer.from(verification.registrationInfo.credentialPublicKey), signatureCount: verification.registrationInfo.counter, transports: credential.response.transports || [], - attestationObject: Buffer.from(verification.registrationInfo.attestationObject), - create_clientDataJSON: Buffer.from(credential.response.clientDataJSON), + attestationObject: credential.response.attestationObject, + create_clientDataJSON: credential.response.clientDataJSON, prfCapable: credential.clientExtensionResults?.prf?.enabled || false, }), ], }; - const userRes = await createUser(newUser, false, ); + const userRes = await createUser(newUser, false,); if (userRes.ok) { console.log("Created user", userRes.val); - res.status(200).send(await initSession(userRes.val)); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(userRes.val.privateData) }) + .send(await initSession(userRes.val)); } else { res.status(500).send({}); } @@ -208,20 +224,20 @@ noAuthUserController.post('/login-webauthn-begin', async (req: Request, res: Res const challenge = challengeRes.unwrap(); const getOptions = webauthn.makeGetOptions({ challenge: challenge.challenge }); - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ challengeId: challenge.id, getOptions, - })); + }); }); noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Response) => { console.log("webauthn login-finish", req.body); const credential = req.body.credential; - const userHandle = base64url.toBuffer(credential.response.userHandle).toString(); - const credentialId = base64url.toBuffer(credential.id); + const userId = UserId.fromUserHandle(credential.response.userHandle); + const credentialId = credential.rawId; - const userRes = await getUserByWebauthnCredential(userHandle, credentialId); + const userRes = await getUserByWebauthnCredential(userId, credentialId); if (userRes.err) { res.status(403).send({}); return; @@ -242,7 +258,17 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re console.log("webauthn login-finish challenge", challenge); const verification = await SimpleWebauthn.verifyAuthenticationResponse({ - response: credential, + response: { + type: credential.type, + id: credential.id, + rawId: credential.id, // SimpleWebauthn requires this base64url encoded + response: { + authenticatorData: base64url.encode(credential.response.authenticatorData), + clientDataJSON: base64url.encode(credential.response.clientDataJSON), + signature: base64url.encode(credential.response.signature), + }, + clientExtensionResults: credential.clientExtensionResults, + }, expectedChallenge: base64url.encode(challenge.challenge), expectedOrigin: config.webauthn.origin, expectedRPID: config.webauthn.rp.id, @@ -262,7 +288,9 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re }); if (updateCredentialRes.ok) { - res.status(200).send(await initSession(user)); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .send(await initSession(user)); } else { res.status(500).send({}); } @@ -274,11 +302,10 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re userController.post('/fcm_token/add', async (req: Request, res: Response) => { - const userDID = req.user.did; - updateUserByDID(userDID, (userEntity, manager) => { + updateUser(req.user.id, (userEntity, manager) => { if (req.body.fcm_token && - req.body.fcm_token != '' && - userEntity.fcmTokenList.filter((fcmTokenEntity) => fcmTokenEntity.value == req.body.fcm_token).length == 0) { + req.body.fcm_token != '' && + userEntity.fcmTokenList.filter((fcmTokenEntity) => fcmTokenEntity.value == req.body.fcm_token).length == 0) { const fcmTokenEntity = new FcmTokenEntity(); fcmTokenEntity.user = userEntity; fcmTokenEntity.value = req.body.fcm_token; @@ -292,22 +319,18 @@ userController.post('/fcm_token/add', async (req: Request, res: Response) => { }) userController.get('/account-info', async (req: Request, res: Response) => { - const userRes = await getUserByDID(req.user.did); + const userRes = await getUser(req.user.id); if (userRes.err) { res.status(403).send({}); return; } const user = userRes.unwrap(); - const keys = jsonParseTaggedBinary(user.keys.toString()); - - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ + uuid: user.uuid, username: user.username, displayName: user.displayName, - did: user.did, hasPassword: user.passwordHash !== null, - publicKey: keys.publicKey, - webauthnUserHandle: user.webauthnUserHandle, webauthnCredentials: (user.webauthnCredentials || []).map(cred => ({ createTime: cred.createTime, credentialId: cred.credentialId, @@ -316,16 +339,11 @@ userController.get('/account-info', async (req: Request, res: Response) => { nickname: cred.nickname, prfCapable: cred.prfCapable, })), - })); + }); }) userController.post('/webauthn/register-begin', async (req: Request, res: Response) => { - const userRes = await updateUserByDID(req.user.did, (userEntity, manager) => { - if (!userEntity.webauthnUserHandle) { - userEntity.webauthnUserHandle = uuid.v4(); - } - return userEntity; - }); + const userRes = await getUser(req.user.id); if (userRes.err) { res.status(403).send({}); @@ -334,7 +352,7 @@ userController.post('/webauthn/register-begin', async (req: Request, res: Respon const user = userRes.unwrap(); const prfSalt = crypto.randomBytes(32); - const challengeRes = await createChallenge("create", user.webauthnUserHandle, prfSalt); + const challengeRes = await createChallenge("create", user.uuid, prfSalt); if (challengeRes.err) { res.status(500).send({}); return; @@ -349,17 +367,17 @@ userController.post('/webauthn/register-begin', async (req: Request, res: Respon }, }); - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ username: user.username, challengeId: challenge.id, createOptions, - })); + }); }); userController.post('/webauthn/register-finish', async (req: Request, res: Response) => { console.log("webauthn register-finish", req.body); - const userRes = await getUserByDID(req.user.did); + const userRes = await getUser(req.user.id); if (userRes.err) { res.status(403).send({}); return; @@ -381,19 +399,28 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo const credential = req.body.credential; const verification = await SimpleWebauthn.verifyRegistrationResponse({ - response: credential, + response: { + type: credential.type, + id: credential.id, + rawId: credential.id, // SimpleWebauthn requires this base64url encoded + response: { + attestationObject: base64url.encode(credential.response.attestationObject), + clientDataJSON: base64url.encode(credential.response.clientDataJSON), + }, + clientExtensionResults: credential.clientExtensionResults, + }, expectedChallenge: base64url.encode(challenge.challenge), expectedOrigin: config.webauthn.origin, expectedRPID: config.webauthn.rp.id, }); if (verification.verified) { - const updateUserRes = await updateUserByDID(user.did, (userEntity, manager) => { + const updateUserRes = await updateUser(user.uuid, (userEntity, manager) => { userEntity.webauthnCredentials = userEntity.webauthnCredentials || []; userEntity.webauthnCredentials.push( newWebauthnCredentialEntity({ credentialId: Buffer.from(verification.registrationInfo.credentialID), - userHandle: Buffer.from(userEntity.webauthnUserHandle), + _userHandle: user.uuid.asUserHandle(), nickname: req.body.nickname, publicKeyCose: Buffer.from(verification.registrationInfo.credentialPublicKey), signatureCount: verification.registrationInfo.counter, @@ -403,16 +430,32 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo prfCapable: credential.clientExtensionResults?.prf?.enabled || false, }, manager) ); - if (req.body.privateData) { - userEntity.privateData = Buffer.from(req.body.privateData); + + const newPrivateData = checkedUpdate( + req.headers['x-private-data-if-match'], + privateDataEtag, + { + currentValue: userEntity.privateData, + newValue: req.body.privateData, + }, + ); + if (newPrivateData.ok) { + userEntity.privateData = newPrivateData.val; + } else { + return Err(UpdateUserErr.PRIVATE_DATA_CONFLICT); } + return userEntity; }); if (updateUserRes.ok) { - res.status(200).send(jsonStringifyTaggedBinary({ - credentialId: credential.id - })); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(updateUserRes.val.privateData) }) + .send({ credentialId: credential.id }); + } else if (updateUserRes.val === UpdateUserErr.PRIVATE_DATA_CONFLICT) { + res.status(412) + .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .send({}); } else { res.status(500).send({}); } @@ -425,7 +468,7 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo userController.post('/webauthn/credential/:id/rename', async (req: Request, res: Response) => { console.log("webauthn rename", req.params.id); - const updateRes = await updateWebauthnCredentialById(req.user.did, req.params.id, (credentialEntity, manager) => { + const updateRes = await updateWebauthnCredentialById(req.user.id, req.params.id, (credentialEntity, manager) => { credentialEntity.nickname = req.body.nickname || null; return credentialEntity; }); @@ -446,16 +489,22 @@ userController.post('/webauthn/credential/:id/rename', async (req: Request, res: userController.post('/webauthn/credential/:id/delete', async (req: Request, res: Response) => { console.log("webauthn delete", req.params.id); - const userRes = await getUserByDID(req.user.did); + const userRes = await getUser(req.user.id); if (userRes.err) { res.status(403).send({}); return; } const user = userRes.unwrap(); - const deleteRes = await deleteWebauthnCredential(user, req.params.id, Buffer.from(req.body.privateData)); + const updatePrivateData: EtagUpdate = { + expectTag: req.headers['x-private-data-if-match'] as string, + newValue: req.body.privateData, + }; + const deleteRes = await deleteWebauthnCredential(user, req.params.id, updatePrivateData); if (deleteRes.ok) { - res.status(204).send(); + res.status(204) + .header({ 'X-Private-Data-ETag': privateDataEtag(updatePrivateData.newValue) }) + .send(); } else { if (deleteRes.val === UpdateUserErr.NOT_EXISTS) { res.status(404).send(); @@ -463,24 +512,82 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: } else if (deleteRes.val === UpdateUserErr.LAST_WEBAUTHN_CREDENTIAL) { res.status(409).send(); + } else if (deleteRes.val === UpdateUserErr.PRIVATE_DATA_CONFLICT) { + res.status(412) + .header({ 'X-Private-Data-ETag': privateDataEtag(updatePrivateData.newValue) }) + .send(); + } else { res.status(500).send(); } } }) +userController.post('/private-data', async (req: Request, res: Response) => { + const updateUserRes = await updateUser(req.user.id, userEntity => { + const newPrivateData = checkedUpdate( + req.headers['x-private-data-if-match'], + privateDataEtag, + { + currentValue: userEntity.privateData, + newValue: req.body, + }, + ); + if (newPrivateData.ok) { + userEntity.privateData = newPrivateData.val; + return Ok(userEntity); + } else { + return Err([UpdateUserErr.PRIVATE_DATA_CONFLICT, userEntity]); + } + }); + + if (updateUserRes.ok) { + res.status(204) + .header({ 'X-Private-Data-ETag': privateDataEtag(updateUserRes.val.privateData) }) + .send(); + } else { + if (updateUserRes.val === UpdateUserErr.NOT_EXISTS) { + res.status(404).send(); + + } else if (updateUserRes.val[0] === UpdateUserErr.PRIVATE_DATA_CONFLICT) { + res.status(412) + .header({ 'X-Private-Data-ETag': privateDataEtag(updateUserRes.val[1].privateData) }) + .send(); + + } else { + res.status(500).send(); + } + } +}); + +userController.get('/private-data', async (req: Request, res: Response) => { + const userRes = await getUser(req.user.id); + if (userRes.ok) { + const privateData = userRes.val.privateData; + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(privateData) }) + .send({ privateData }); + } else { + if (userRes.val === GetUserErr.NOT_EXISTS) { + res.status(404).send(); + + } else { + res.status(500).send(); + } + } +}); + userController.delete('/', async (req: Request, res: Response) => { - const userDID = req.user.did; try { await runTransaction(async (entityManager: EntityManager) => { // Note: this executes all four branches before checking if any failed. // ts-results does not seem to provide an async-optimized version of Result.all(), // and it turned out nontrivial to write one that preserves the Ok and Err types like Result.all() does. return Result.all( - await deleteAllFcmTokensForUser(userDID, { entityManager }), - await deleteAllCredentialsWithHolderDID(userDID, { entityManager }), - await deleteAllPresentationsWithHolderDID(userDID, { entityManager }), - await deleteUserByDID(userDID, { entityManager }), + await deleteAllFcmTokensForUser(req.user.id, { entityManager }), + await deleteAllCredentialsWithHolderDID(req.user.did, { entityManager }), + await deleteAllPresentationsWithHolderDID(req.user.did, { entityManager }), + await deleteUser(req.user.id, { entityManager }), ); }); @@ -489,25 +596,5 @@ userController.delete('/', async (req: Request, res: Response) => { return res.status(400).send({ result: e }) } }); -// /** -// * expect 'alg' query parameter -// */ -// userController.get('/keys/public', AuthMiddleware, async (req: Request, res: Response) => { -// const did = req.user?.did; -// const algorithm = req.query["alg"] as string; -// if (did == undefined) { -// res.status(401).send({ err: 'UNAUTHORIZED' }); -// return; -// } -// const alg: SigningAlgorithm = algorithm as SigningAlgorithm; -// const result = await getPublicKey(did, algorithm as SigningAlgorithm); -// if (!result) { -// res.status(500).send(); -// return; -// } -// const { publicKeyJwk } = result; - -// res.send({ publicKeyJwk }); -// }); export default noAuthUserController; diff --git a/src/routers/verifiers.router.ts b/src/routers/verifiers.router.ts index 26e624a..91662fe 100644 --- a/src/routers/verifiers.router.ts +++ b/src/routers/verifiers.router.ts @@ -12,4 +12,4 @@ verifiersRouter.get('/all', async (req, res) => { res.send({ verifiers: await verifiersRegistryService.getAllVerifiers() }); }); -export default verifiersRouter; \ No newline at end of file +export default verifiersRouter; diff --git a/src/services/ClientKeystoreService.ts b/src/services/ClientKeystoreService.ts index dcfa80c..bb859f0 100644 --- a/src/services/ClientKeystoreService.ts +++ b/src/services/ClientKeystoreService.ts @@ -3,11 +3,11 @@ import { inject, injectable } from "inversify"; import "reflect-metadata"; import { Err, Ok, Result } from "ts-results"; -import { AdditionalKeystoreParameters, RegistrationParams, SocketManagerServiceInterface, WalletKeystore, WalletKeystoreErr } from "./interfaces"; +import { AdditionalKeystoreParameters, SocketManagerServiceInterface, WalletKeystore, WalletKeystoreErr } from "./interfaces"; import { TYPES } from "./types"; import config from "../../config"; import { SignatureAction, ServerSocketMessage } from "./shared.types"; -import { WalletKey } from "@wwwallet/ssi-sdk"; +import { UserId } from "../entities/user.entity"; @@ -23,30 +23,7 @@ export class ClientKeystoreService implements WalletKeystore { ) { } - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { - let message_id_sent = randomUUID(); - const msg = { - message_id: message_id_sent, - request: { - action: SignatureAction.createIdToken, - nonce: nonce, - audience: audience - } - } - await this.socketManagerService.send(userDid, msg as ServerSocketMessage) - - const result = await this.socketManagerService.expect(userDid, message_id_sent, SignatureAction.createIdToken); - if (result.err) { - return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); - } - const { message: { message_id, response } } = result.unwrap(); - if (response.action == SignatureAction.createIdToken) { - return Ok({ id_token: response.id_token }); - } - return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); - } - - async signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise> { + async signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise> { let message_id_sent = randomUUID(); const msg = { message_id: message_id_sent, @@ -57,9 +34,9 @@ export class ClientKeystoreService implements WalletKeystore { verifiableCredentials: verifiableCredentials } } - await this.socketManagerService.send(userDid, msg as ServerSocketMessage) + await this.socketManagerService.send(userId, msg as ServerSocketMessage) - const result = await this.socketManagerService.expect(userDid, message_id_sent, SignatureAction.signJwtPresentation); + const result = await this.socketManagerService.expect(userId, message_id_sent, SignatureAction.signJwtPresentation); if (result.err) { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } @@ -70,7 +47,7 @@ export class ClientKeystoreService implements WalletKeystore { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } - async generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise> { + async generateOpenid4vciProof(userId: UserId, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise> { let message_id_sent = randomUUID(); const msg = { message_id: message_id_sent, @@ -81,8 +58,8 @@ export class ClientKeystoreService implements WalletKeystore { } } console.log("MessageID = ", message_id_sent) - await this.socketManagerService.send(userDid, msg as ServerSocketMessage); - const result = await this.socketManagerService.expect(userDid, message_id_sent, SignatureAction.generateOpenid4vciProof); + await this.socketManagerService.send(userId, msg as ServerSocketMessage); + const result = await this.socketManagerService.expect(userId, message_id_sent, SignatureAction.generateOpenid4vciProof); if (result.err) { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } @@ -93,4 +70,4 @@ export class ClientKeystoreService implements WalletKeystore { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } -} \ No newline at end of file +} diff --git a/src/services/DatabaseKeystoreService.ts b/src/services/DatabaseKeystoreService.ts index db41d51..de40efd 100644 --- a/src/services/DatabaseKeystoreService.ts +++ b/src/services/DatabaseKeystoreService.ts @@ -7,7 +7,7 @@ import { Err, Ok, Result } from "ts-results"; import { SignVerifiablePresentationJWT, WalletKey } from "@wwwallet/ssi-sdk"; import { AdditionalKeystoreParameters, DidKeyUtilityService, RegistrationParams, WalletKeystore, WalletKeystoreErr } from "./interfaces"; import { verifiablePresentationSchemaURL } from "../util/util"; -import { getUserByDID } from "../entities/user.entity"; +import { getUser, UserId } from "../entities/user.entity"; import { TYPES } from "./types"; import config from "../../config"; @@ -32,34 +32,9 @@ export class DatabaseKeystoreService implements WalletKeystore { } } - - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { - const user = (await getUserByDID(userDid)).unwrap(); - const keys = JSON.parse(user.keys.toString()) as WalletKey; - - if (!keys.privateKey) { - return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); - } - - const privateKey = await importJWK(keys.privateKey, keys.alg); - const jws = await new SignJWT({ nonce: nonce }) - .setProtectedHeader({ - alg: this.algorithm, - typ: "JWT", - kid: keys.verificationMethod, - }) - .setSubject(user.did) - .setIssuer(user.did) - .setExpirationTime('1m') - .setAudience(audience) - .setIssuedAt() - .sign(privateKey); - - return Ok({ id_token: jws }); - } - async signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise> { - const user = (await getUserByDID(userDid)).unwrap(); + async signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise> { + const user = (await getUser(userId)).unwrap(); const keys = JSON.parse(user.keys.toString()) as WalletKey; if (!keys.privateKey) { return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); @@ -90,8 +65,8 @@ export class DatabaseKeystoreService implements WalletKeystore { return Ok({ vpjwt: jws }); } - async generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise> { - const user = (await getUserByDID(userDid)).unwrap(); + async generateOpenid4vciProof(userId: UserId, audience: string, nonce: string, additionalParameters: AdditionalKeystoreParameters): Promise> { + const user = (await getUser(userId)).unwrap(); const keys = JSON.parse(user.keys.toString()) as WalletKey; if (!keys.privateKey) { return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); diff --git a/src/services/EBSIDidKeyUtilityService.ts b/src/services/EBSIDidKeyUtilityService.ts index a6155a2..a5f0e9f 100644 --- a/src/services/EBSIDidKeyUtilityService.ts +++ b/src/services/EBSIDidKeyUtilityService.ts @@ -18,4 +18,4 @@ export class EBSIDidKeyUtilityService implements DidKeyUtilityService { const naturalPersonWallet: NaturalPersonWallet = await new NaturalPersonWallet().createWallet(config.alg); return { did: naturalPersonWallet.key.did, key: naturalPersonWallet.key }; } -} \ No newline at end of file +} diff --git a/src/services/OpenidForCredentialIssuanceService.ts b/src/services/OpenidForCredentialIssuanceService.ts index 99de8d1..0f8d3ee 100644 --- a/src/services/OpenidForCredentialIssuanceService.ts +++ b/src/services/OpenidForCredentialIssuanceService.ts @@ -1,7 +1,6 @@ import axios from "axios"; import * as _ from 'lodash'; import base64url from "base64url"; -import qs from "qs"; import { injectable, inject } from "inversify"; import "reflect-metadata"; import { Err, Ok, Result } from "ts-results"; @@ -9,19 +8,16 @@ import { Err, Ok, Result } from "ts-results"; import { LegalPersonEntity, getLegalPersonByDID, getLegalPersonByUrl } from "../entities/LegalPerson.entity"; import { CredentialIssuerMetadata, CredentialResponseSchemaType, CredentialSupportedJwtVcJson, GrantType, OpenidConfiguration, TokenResponseSchemaType, VerifiableCredentialFormat } from "../types/oid4vci"; import config from "../../config"; -import { getUserByDID } from "../entities/user.entity"; +import { getUser, UserId } from "../entities/user.entity"; import { sendPushNotification } from "../lib/firebase"; import { generateCodeChallengeFromVerifier, generateCodeVerifier } from "../util/util"; import { createVerifiableCredential } from "../entities/VerifiableCredential.entity"; -import { getLeafNodesWithPath } from "../lib/leafnodepaths"; import { TYPES } from "./types"; -import { IssuanceErr, OpenidCredentialReceiving, WalletKeystore, WalletKeystoreErr } from "./interfaces"; -import { WalletKeystoreRequest, SignatureAction } from "./shared.types"; +import { IssuanceErr, OpenidCredentialReceiving, WalletKeystore } from "./interfaces"; import { randomUUID } from 'node:crypto'; -import { error } from "node:console"; type IssuanceState = { - userDid: string; // Before Authorization Req + userId: UserId; // Before Authorization Req legalPerson: LegalPersonEntity; // Before Authorization Req credentialIssuerMetadata: CredentialIssuerMetadata; // Before Authorization Req openidConfiguration: OpenidConfiguration; // Before Authorization Req @@ -40,9 +36,9 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei // identifierService: IdentifierService = new IdentifierService(); // legalPersonService: LegalPersonService = new LegalPersonService(); - - // key: userDid + + // key: UserEntity.uuid public states = new Map(); constructor( @@ -50,41 +46,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei ) { } - async getIssuerState(userDid: string): Promise<{ issuer_state?: string, error?: Error; }> { - const state = this.states.get(userDid); - if (!state) { - return { issuer_state: null, error: new Error("No state found") }; - } - if (!state.issuer_state) { - return { issuer_state: null, error: new Error("No issuer_state found in state") }; - } - - return { issuer_state: state.issuer_state, error: null }; - } - - async getAvailableSupportedCredentials(legalPersonDID: string): Promise> { - const lp = (await getLegalPersonByDID(legalPersonDID)).unwrapOr(new Error("Not found")); - if (lp instanceof Error) { - return []; - } - const issuerUrlString = lp.url; - const credentialIssuerMetadata = await axios.get(issuerUrlString + "/.well-known/openid-credential-issuer"); - - const options = credentialIssuerMetadata.data.credentials_supported.map((val) => { - return { id: val.id, displayName: val.display[0].name }; - }) - return options as Array<{id: string, displayName: string}>; - } - - /** - * - * @param userDid - * @param legalPersonDID - * @returns - * @throws - */ - async generateAuthorizationRequestURL(userDid: string, credentialOfferURL?: string, legalPersonDID?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }> { - console.log("generateAuthorizationRequestURL userDid = ", userDid); + async generateAuthorizationRequestURL(userId: UserId, credentialOfferURL?: string, legalPersonDID?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }> { + console.log("generateAuthorizationRequestURL userId = ", userId); console.log("LP = ", legalPersonDID); let issuerUrlString: string | null = null; let credential_offer = null; @@ -127,8 +90,9 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei lp = (await getLegalPersonByUrl(credentialIssuerURL)).unwrap(); if (!lp) { + const user = (await getUser(userId)).unwrap(); // as client id we are going to use the userDid - lp = { did: null, friendlyName: "Tmp", client_id: userDid, id: -1, url: credentialIssuerURL } + lp = { did: null, friendlyName: "Tmp", client_id: user.did, id: -1, url: credentialIssuerURL } } issuerUrlString = lp.url; @@ -139,7 +103,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei throw new Error("No issuer url is defined"); } - + const credentialIssuerMetadata = (await axios.get(issuerUrlString + "/.well-known/openid-credential-issuer")).data as CredentialIssuerMetadata; console.log("Credential issuer metadata") @@ -159,8 +123,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei console.log("Credential offer = ", credential_offer) if (credential_offer && credential_offer.grants["urn:ietf:params:oauth:grant-type:pre-authorized_code"]) { - this.states.set(userDid, { - userDid, + this.states.set(userId.id, { + userId, credentialIssuerMetadata: credentialIssuerMetadata, openidConfiguration: authorizationServerConfig, legalPerson: lp, @@ -173,16 +137,16 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei console.log("Redirecting to ... ", config.walletClientUrl + `?preauth=true&ask_for_pin=${user_pin_required}`) return { preauth: true, ask_for_pin: user_pin_required } } - - - - + + + + const authorizationRequestURL = new URL(authorizationServerConfig.authorization_endpoint); authorizationRequestURL.searchParams.append("scope", "openid"); authorizationRequestURL.searchParams.append("client_id", lp.client_id); - + authorizationRequestURL.searchParams.append("redirect_uri", config.walletClientUrl); authorizationRequestURL.searchParams.append("authorization_details", JSON.stringify(authorizationDetails)); @@ -194,8 +158,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei authorizationRequestURL.searchParams.append("issuer_state", issuer_state); authorizationRequestURL.searchParams.append("client_metadata", JSON.stringify(client_metadata)); - this.states.set(userDid, { - userDid, + this.states.set(userId.id, { + userId, authorization_details: authorizationDetails, credentialIssuerMetadata: credentialIssuerMetadata, openidConfiguration: authorizationServerConfig, @@ -210,10 +174,10 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei - public async requestCredentialsWithPreAuthorizedGrant(userDid: string, user_pin: string): Promise<{error?: string}> { - let state = this.states.get(userDid) + public async requestCredentialsWithPreAuthorizedGrant(userId: UserId, user_pin: string): Promise<{error?: string}> { + let state = this.states.get(userId.id) state = { ...state, user_pin: user_pin }; - this.states.set(userDid, state); // save state with pin + this.states.set(userId.id, state); // save state with pin return this.tokenRequest(state).then(tokenResponse => { @@ -222,8 +186,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei throw new Error("Token response is undefined"); } state = { ...state, tokenResponse } - this.states.set(userDid, state); - this.credentialRequests(userDid, state).catch(e => { + this.states.set(userId.id, state); + this.credentialRequests(userId, state).catch(e => { console.error("Credential requests failed with error : ", e) }); return {}; @@ -240,12 +204,12 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei } /** - * + * * @param authorizationResponseURL * @throws */ - public async handleAuthorizationResponse(userDid: string, authorizationResponseURL: string): Promise> { - const currentState = this.states.get(userDid); + public async handleAuthorizationResponse(userId: UserId, authorizationResponseURL: string): Promise> { + const currentState = this.states.get(userId.id); if (!currentState) { return Err(IssuanceErr.STATE_NOT_FOUND); } @@ -260,16 +224,16 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei return; } let newState = { ...currentState, code }; - this.states.set(userDid, newState); + this.states.set(userId.id, newState); const tokenResponse = await this.tokenRequest(newState); if (!tokenResponse) { return; } newState = { ...newState, tokenResponse } - this.states.set(userDid, newState); + this.states.set(userId.id, newState); try { - await this.credentialRequests(userDid, newState); + await this.credentialRequests(userId, newState); } catch (e) { console.error("Credential requests failed with error : ", e) } @@ -279,12 +243,12 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei /** * @throws - * @param state - * @returns + * @param state + * @returns */ private async tokenRequest(state: IssuanceState): Promise { // const basicAuthorizationB64 = Buffer.from(`${state.legalPerson.client_id}:${state.legalPerson.client_secret}`).toString("base64"); - const httpHeader = { + const httpHeader = { // "authorization": `Basic ${basicAuthorizationB64}`, "Content-Type": "application/x-www-form-urlencoded" }; @@ -328,17 +292,17 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei /** * @throws */ - private async credentialRequests(userDid: string, state: IssuanceState): Promise> { + private async credentialRequests(userId: UserId, state: IssuanceState): Promise> { const c_nonce = state.tokenResponse.c_nonce; - const res = await this.walletKeystoreManagerService.generateOpenid4vciProof(userDid, state.credentialIssuerMetadata.credential_issuer, c_nonce); + const res = await this.walletKeystoreManagerService.generateOpenid4vciProof(userId, state.credentialIssuerMetadata.credential_issuer, c_nonce); console.log("Result proof generation = ", res) if (res.ok) { const { proof_jwt } = res.val; - return Ok(await this.finishCredentialRequests(userDid, state, proof_jwt)); + return Ok(await this.finishCredentialRequests(userId, state, proof_jwt)); } } - private async finishCredentialRequests(userDid: string, state: IssuanceState, proof_jwt: string) { + private async finishCredentialRequests(userId: UserId, state: IssuanceState, proof_jwt: string) { const credentialEndpoint = state.credentialIssuerMetadata.credential_endpoint; const httpHeader = { @@ -364,7 +328,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei .map(response => { console.error(`Failed credential (status, body) : (${response.response.status}, ${JSON.stringify(response.response.data)})`, ); }); - + let credentialResponses = responses .filter(res => res.status == 'fulfilled') .map((res) => @@ -372,7 +336,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei ); // Prevent duplicate credential acceptance - this.states.delete(userDid); + this.states.delete(userId.id); for (const cr of credentialResponses) { if (cr.acceptance_token) @@ -392,12 +356,12 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei // Deferred Credential only private async checkConstantlyForPendingCredential(state: IssuanceState, acceptance_token: string) { - const defferedCredentialReqHeader = { + const defferedCredentialReqHeader = { "authorization": `Bearer ${acceptance_token}`, }; - + axios.post(state.credentialIssuerMetadata.deferred_credential_endpoint, - {}, + {}, { headers: defferedCredentialReqHeader } ) .then((res) => { this.handleCredentialStorage(state, res.data); @@ -408,11 +372,11 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei }, 2000); }) - + } private async handleCredentialStorage(state: IssuanceState, credentialResponse: CredentialResponseSchemaType) { - const userRes = await getUserByDID(state.userDid); + const userRes = await getUser(state.userId); if (userRes.err) { return; } @@ -423,18 +387,18 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei const credentialPayload = JSON.parse(base64url.decode(credentialResponse.credential.split('.')[1])) const type = credentialPayload.vc.type as string[]; const metadata = (await axios.get(legalPerson.url + "/.well-known/openid-credential-issuer")).data as CredentialIssuerMetadata; - - + + let logoUrl = config.url + "/alt-vc-logo.png"; let background_color = "#D3D3D3"; const supportedCredential = metadata.credentials_supported.filter(cs => cs.format == credentialResponse.format && _.isEqual(cs.types, type))[0]; if (supportedCredential) { - if (supportedCredential.display && + if (supportedCredential.display && supportedCredential.display.length != 0 && supportedCredential.display[0]?.logo && supportedCredential.display[0]?.logo?.url) { - + logoUrl = supportedCredential.display[0].logo.url; } diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index eb7e550..904ba7d 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -8,12 +8,12 @@ import { z } from 'zod'; import { Err, Ok, Result } from "ts-results"; import { InputDescriptorType, Verify } from "@wwwallet/ssi-sdk"; -import { HandleOutboundRequestError, OpenidCredentialReceiving, OutboundCommunication, SendResponseError, WalletKeystore, WalletKeystoreErr } from "./interfaces"; +import { HandleOutboundRequestError, OutboundCommunication, SendResponseError, WalletKeystore } from "./interfaces"; import { TYPES } from "./types"; import { OutboundRequest } from "./types/OutboundRequest"; import { getAllVerifiableCredentials } from "../entities/VerifiableCredential.entity"; import { createVerifiablePresentation } from "../entities/VerifiablePresentation.entity"; -import { getUserByDID } from "../entities/user.entity"; +import { getUser, UserId } from "../entities/user.entity"; import { VerifierRegistryService } from "./VerifierRegistryService"; import { randomUUID, createHash } from "node:crypto"; import config from "../../config"; @@ -21,7 +21,6 @@ import { WalletKeystoreRequest, SignatureAction } from "./shared.types"; import { HasherAlgorithm, HasherAndAlgorithm, - SaltGenerator, SdJwt, } from '@sd-jwt/core'; @@ -81,25 +80,24 @@ type VerificationState = { @injectable() export class OpenidForPresentationService implements OutboundCommunication { - // key: did + // key: UserEntity.uuid states = new Map(); constructor( @inject(TYPES.WalletKeystoreManagerService) private walletKeystoreManagerService: WalletKeystore, @inject(TYPES.VerifierRegistryService) private verifierRegistryService: VerifierRegistryService, - @inject(TYPES.OpenidForCredentialIssuanceService) private OpenidCredentialReceivingService: OpenidCredentialReceiving ) { } - async initiateVerificationFlow(userDid: string, verifierId: number, scopeName: string): Promise<{ redirect_to?: string }> { + async initiateVerificationFlow(userId: UserId, verifierId: number, scopeName: string): Promise<{ redirect_to?: string }> { const verifier = (await this.verifierRegistryService.getAllVerifiers()).filter(ver => ver.id == verifierId)[0]; - console.log("User did = ", userDid) - const userFetchRes = await getUserByDID(userDid); + console.log("User id = ", userId) + const userFetchRes = await getUser(userId); if (userFetchRes.err) { return {}; } const holder_state = randomUUID(); - this.states.set(userDid, { holder_state }); + this.states.set(userId.id, { holder_state }); const user = userFetchRes.unwrap(); const url = new URL(verifier.url); @@ -110,13 +108,8 @@ export class OpenidForPresentationService implements OutboundCommunication { url.searchParams.append("state", holder_state); return { redirect_to: url.toString() }; } - - async handleRequest(userDid: string, requestURL: string, camera_was_used: boolean): Promise> { - try { - return await this.parseIdTokenRequest(userDid, requestURL); - } - catch(err) { - } + + async handleRequest(userId: UserId, requestURL: string, camera_was_used: boolean): Promise> { try { const url = new URL(requestURL); @@ -125,8 +118,8 @@ export class OpenidForPresentationService implements OutboundCommunication { const jsonParams = Object.fromEntries(paramEntries); authorizationRequestSchema.parse(jsonParams); // will throw error if input is not conforming to the schema - this.states.set(userDid, { camera_was_used: camera_was_used }) - const result = await this.parseAuthorizationRequest(userDid, requestURL); + this.states.set(userId.id, { camera_was_used: camera_was_used }) + const result = await this.parseAuthorizationRequest(userId, requestURL); if (result.err) { return Err(result.val); } @@ -145,9 +138,9 @@ export class OpenidForPresentationService implements OutboundCommunication { } - async sendResponse(userDid: string, selection: Map): Promise> { + async sendResponse(userId: UserId, selection: Map): Promise> { try { - return await this.generateAuthorizationResponse(userDid, selection) + return await this.generateAuthorizationResponse(userId, selection) } catch(err) { console.error("Failed to generate authorization response.\nError details: ", err); @@ -155,118 +148,13 @@ export class OpenidForPresentationService implements OutboundCommunication { } } - - - private async parseIdTokenRequest(userDid: string, authorizationRequestURL: string): Promise> { - console.log("parseIdTokenRequest userDid:", userDid) - + private async parseAuthorizationRequest(userId: UserId, authorizationRequestURL: string): Promise, verifierDomainName: string}, HandleOutboundRequestError>> { + console.log("parseAuthorizationRequest userId = ", userId) let client_id: string, response_uri: string, nonce: string, presentation_definition: PresentationDefinition | null, state: string | null; - - console.log("Pure params = ", new URL(authorizationRequestURL)) - try { - const searchParams = await this.authorizationRequestSearchParams(authorizationRequestURL); - console.log("SEARCH params = ", searchParams) - client_id = searchParams.client_id; - response_uri = searchParams.response_uri; - nonce = searchParams.nonce; - presentation_definition = searchParams.presentation_definition - state = searchParams.state; - console.log("Pre = ", presentation_definition) - if (searchParams.presentation_definition) { - console.log("Presentation definition is included") - throw new Error("This is not an id token request because presentation definition is included"); - } - } - catch(error) { - throw new Error(`Error fetching authorization request search params: ${error}`); - } - - - const currentState = this.states.get(userDid); - this.states.set(userDid, { - ...currentState, - audience: client_id, - nonce, - response_uri, - state, - }); - const idTokenResult = await this.walletKeystoreManagerService.createIdToken(userDid, nonce, client_id); - if (idTokenResult.ok) { - const { id_token } = idTokenResult.val; - return Ok(await this.finishParseIdTokenRequest(userDid, state, response_uri, id_token)); - } else if (idTokenResult.val === WalletKeystoreErr.KEYS_UNAVAILABLE) { - return Err({ action: SignatureAction.createIdToken, nonce, audience: client_id }); - } - } - - private async finishParseIdTokenRequest(userDid: string, state: string, redirect_uri: string, id_token: string): Promise<{ redirect_to: string }> { - const { issuer_state } = await this.OpenidCredentialReceivingService.getIssuerState(userDid); - - const params = { - id_token, - state: state, - issuer_state: issuer_state - }; - - console.log("Params = ", params) - console.log("RedirectURI = ", redirect_uri) - const encodedParams = qs.stringify(params); - const { newLocation } = await axios.post(redirect_uri, encodedParams, { maxRedirects: 0, headers: { "Content-Type": "application/x-www-form-urlencoded" }}) - .then(success => { - console.log("url = ", success.config.headers) - console.log("body = ", success.data) - console.log(success.status) - const msg = { - error: "Direct post error", - error_description: "Failed to redirect after direct post" - }; - console.error(msg); - // console.log("Sucess = ", success.data) - return { newLocation: null } - }) - .catch(e => { - console.log("ERR"); - console.log("UNKNOWN") - if (e.response) { - console.log("UNKNOWN = ", e.response.data) - - if (e.response.headers.location) { - console.log("Loc: ", e.response.headers.location); - const newLocation = e.response.headers.location as string; - console.error("Body of Error = ", e.response.data) - const url = new URL(newLocation) - console.log("Pure url of loc: ", url) - return { newLocation } - } - else { - return { newLocation: null } - } - - } - }); - console.log("New loc : ", newLocation) - // check if newLocation is null - return { redirect_to: newLocation } - } - - /** - * @throws - * @param userDid - * @param authorizationRequestURL - * @returns - */ - private async parseAuthorizationRequest(userDid: string, authorizationRequestURL: string): Promise, verifierDomainName: string}, HandleOutboundRequestError>> { - console.log("parseAuthorizationRequest userDid = ", userDid) - const { did } = (await getUserByDID(userDid)).unwrap(); - let client_id: string, - response_uri: string, - nonce: string, - presentation_definition: PresentationDefinition | null, - state: string | null; try { console.log("All search params = ", new URL(authorizationRequestURL).searchParams) const params = new URL(authorizationRequestURL).searchParams; @@ -290,8 +178,8 @@ export class OpenidForPresentationService implements OutboundCommunication { catch(error) { throw new Error(`Error fetching authorization request search params: ${error}`); } - const currentState = this.states.get(userDid); - this.states.set(userDid, { + const currentState = this.states.get(userId.id); + this.states.set(userId.id, { ...currentState, presentation_definition, audience: client_id, @@ -301,11 +189,11 @@ export class OpenidForPresentationService implements OutboundCommunication { }); - console.log("State = ", this.states.get(userDid)) + console.log("State = ", this.states.get(userId.id)) console.log("Definition = ", presentation_definition) - + let descriptors: InputDescriptorType[]; try { descriptors = JSONPath({ @@ -317,8 +205,10 @@ export class OpenidForPresentationService implements OutboundCommunication { throw new Error(`Error fetching input descriptors from presentation_definition: ${error}`); } + const user = (await getUser(userId)).unwrap(); + try { - const verifiableCredentialsRes = await getAllVerifiableCredentials(did); + const verifiableCredentialsRes = await getAllVerifiableCredentials(user.did); if (verifiableCredentialsRes.err) { throw "Failed to fetch credentials" } @@ -370,20 +260,20 @@ export class OpenidForPresentationService implements OutboundCommunication { /** - * selection: (key: descriptor_id, value: credentialIdentifier from VerifiableCredential DB entity) - */ - private async generateVerifiablePresentation(selection: Map, presentation_definition: PresentationDefinition, userDid: string): Promise> { - + * selection: (key: descriptor_id, value: credentialIdentifier from VerifiableCredential DB entity) + */ + private async generateVerifiablePresentation(selection: Map, presentation_definition: PresentationDefinition, userId: UserId): Promise> { + const hasherAndAlgorithm: HasherAndAlgorithm = { hasher: (input: string) => createHash('sha256').update(input).digest(), algorithm: HasherAlgorithm.Sha256 } - + /** - * - * @param paths example: [ '$.credentialSubject.image', '$.credentialSubject.grade', '$.credentialSubject.val.x' ] - * @returns example: { credentialSubject: { image: true, grade: true, val: { x: true } } } - */ + * + * @param paths example: [ '$.credentialSubject.image', '$.credentialSubject.grade', '$.credentialSubject.val.x' ] + * @returns example: { credentialSubject: { image: true, grade: true, val: { x: true } } } + */ const generatePresentationFrameForPaths = (paths) => { const result = {}; @@ -403,10 +293,11 @@ export class OpenidForPresentationService implements OutboundCommunication { }); return result; }; - let vcListRes = await getAllVerifiableCredentials(userDid); - if (vcListRes.err) { - throw "Failed to fetch credentials"; - } + const user = (await getUser(userId)).unwrap(); + let vcListRes = await getAllVerifiableCredentials(user.did); + if (vcListRes.err) { + throw "Failed to fetch credentials"; + } const allSelectedCredentialIdentifiers = Array.from(selection.values()); const filteredVCEntities = vcListRes @@ -435,15 +326,15 @@ export class OpenidForPresentationService implements OutboundCommunication { else { selectedVCs.push(vcEntity.credential); } - + } - - const fetchedState = this.states.get(userDid); + + const fetchedState = this.states.get(userId.id); console.log(fetchedState); const { audience, nonce } = fetchedState; - const result = await this.walletKeystoreManagerService.signJwtPresentation(userDid, nonce, audience, selectedVCs); + const result = await this.walletKeystoreManagerService.signJwtPresentation(userId, nonce, audience, selectedVCs); if (!result.ok) { return Err({ action: SignatureAction.signJwtPresentation, @@ -456,31 +347,31 @@ export class OpenidForPresentationService implements OutboundCommunication { return Ok(result.val.vpjwt); } - private async generateAuthorizationResponse(userDid: string, selection: Map): Promise> { - console.log("generateAuthorizationResponse userDid = ", userDid) + private async generateAuthorizationResponse(userId: UserId, selection: Map): Promise> { + console.log("generateAuthorizationResponse userId = ", userId) const allSelectedCredentialIdentifiers = Array.from(selection.values()); - const { did } = (await getUserByDID(userDid)).unwrap(); + const { did } = (await getUser(userId)).unwrap(); console.log("Verifiable credentials map = ", selection) - let vcListRes = await getAllVerifiableCredentials(did); + const user = (await getUser(userId)).unwrap(); + let vcListRes = await getAllVerifiableCredentials(user.did); if (vcListRes.err) { throw "Failed to fetch credentials" } const filteredVCEntities = vcListRes.unwrap() - .filter((vc) => + .filter((vc) => allSelectedCredentialIdentifiers.includes(vc.credentialIdentifier) ); - const filteredVCJwtList = filteredVCEntities.map((vc) => vc.credential); - + try { - const fetchedState = this.states.get(userDid); - const vp_token_result = await this.generateVerifiablePresentation(selection, fetchedState.presentation_definition, userDid); + const fetchedState = this.states.get(userId.id); + const vp_token_result = await this.generateVerifiablePresentation(selection, fetchedState.presentation_definition, userId); if (vp_token_result.err) { return Err(vp_token_result.val); } const vp_token: string = vp_token_result.val as string; - const {presentation_definition, response_uri, state} = this.states.get(userDid); + const {presentation_definition, response_uri, state} = this.states.get(userId.id); // console.log("vp token = ", vp_token) // console.log("Presentation definition from state is = "); // console.dir(presentation_definition, { depth: null }); @@ -488,9 +379,9 @@ export class OpenidForPresentationService implements OutboundCommunication { if(matchesPresentationDefinitionRes == null) { throw new Error("Credentials presented do not match presentation definition requested"); } - + const {presentationSubmission} = matchesPresentationDefinitionRes; - + // let counter = 0 // for (let i = 0; i < presentationSubmission.descriptor_map.length; i++) { @@ -549,7 +440,7 @@ export class OpenidForPresentationService implements OutboundCommunication { format: "jwt_vp" }); - const verificationState = this.states.get(userDid); + const verificationState = this.states.get(userId.id); if (verificationState && verificationState.camera_was_used) { return Ok({ }) } @@ -560,209 +451,4 @@ export class OpenidForPresentationService implements OutboundCommunication { } } - /** - * Extract a Presentation Definition contained in an Authorization Request URL. - * The Presentation Definition may be contained as a plain, uri-encoded JSON object in the presentation_definition parameter, - * or as the response of an API indicated on the presentation_definition_uri parameter. - * Usage of both presentation_definition and presentation_definition_uri parameters is invalid. - * The function checks which of the two url parameters is present, and handles fetching appropriately. - * After a presentation definition has been fetched, its validity is examined. - * If the presentation definition is valid, it is returned. - * @param authorizationRequestURL - * @returns PresentationDefinition - * @throws InvalidAuthorizationRequestURLError - * @throws InvalidPresentationDefinitionURIError - * @throws InvalidPresentationDefinitionError - */ - private async fetchPresentationDefinition(authorizationRequestURL: URL): Promise { - - const searchParams = authorizationRequestURL.searchParams; - console.log("Params = ", searchParams) - let presentation_definition = JSON.parse(searchParams.get("presentation_definition")); - let presentation_definition_uri = searchParams.get("presentation_definition_uri"); - - const request = searchParams.get("request"); - console.log("Request payload = ", JSON.parse(base64url.decode(request.split('.')[1]))) - const requestPayload: any = request ? JSON.parse(base64url.decode(request.split('.')[1])) : null; - if(requestPayload && requestPayload.presentation_definition) - presentation_definition = requestPayload.presentation_definition; - if(requestPayload && requestPayload.presentation_definition_uri) - presentation_definition_uri = requestPayload.presentation_definition_uri; - - if(presentation_definition && presentation_definition_uri) { - const error = "Both presentation_definition and presentation_definition_uri parameters in authorization request URL"; - console.error(error); - throw new Error(`Invalid Authorization Request URL: ${error}`); - } - - if(!presentation_definition && !presentation_definition_uri) { - const error = "Neither presentation_definition nor presentation_definition_uri parameters in authorization request URL"; - console.error(error); - throw new Error(`Invalid Authorization Request URL: ${error}`); - } - - let presentationDefinition: PresentationDefinition; - if(presentation_definition) { - presentationDefinition = presentation_definition; - console.log("Parsed presentation definition = " , presentationDefinition) - } - else { - - try { - presentationDefinition = await this.fetchPresentationDefinitionUri(presentation_definition_uri); - } - catch(error) { - console.error(`Error fetching presentation definition from URI: ${error}`); - throw new Error(`Error fetching presentation definition from URI: ${error}`); - } - } - // TODO: Check Presentation Definition validity - return presentationDefinition; - - } - - private async fetchPresentationDefinitionUri(uri: string): Promise { - - // test if PresentationDefinitionUri is malformed string - try { - new URL(uri); - } - catch(error) { - console.error(`Presentation Definition URI is invalid.`) - throw new Error(`Invalid PresentationDefinitionURI: ${error}`); - } - - const fetchPresentationDefinitionRes = await axios.get(uri.toString()); - if(fetchPresentationDefinitionRes.status !== 200) { - console.error(`Error fetching Presentation Definition from URI: ${fetchPresentationDefinitionRes.data}`); - throw new Error(`Error fetching Presentation Definition from URI`); - } - - return fetchPresentationDefinitionRes.data; - } - - - /** - * Handle Authorization Request search Parameters. - * @param authorizationRequest a string of the authorization request URL - * @returns An object containing Authorization Request Parameters - */ - private async authorizationRequestSearchParams(authorizationRequest: string) { - - // let response_type, client_id, redirect_uri, scope, response_mode, presentation_definition, nonce; - - // Attempt to convert authorizationRequest to URL form, in order to parse searchparams easily - // An error will be thrown if the URL is invalid - let authorizationRequestUrl: URL; - try { - authorizationRequestUrl = new URL(authorizationRequest); - } - catch(error) { - throw new Error(`Invalid Authorization Request URL: ${error}`); - } - - // const variables are REQUIRED authorization request parameters and they must exist outside the "request" parameter - const response_type = authorizationRequestUrl.searchParams.get("response_type"); - const client_id = authorizationRequestUrl.searchParams.get("client_id"); - const response_uri = authorizationRequestUrl.searchParams.get("response_uri") ?? authorizationRequestUrl.searchParams.get("redirect_uri"); - const scope = authorizationRequestUrl.searchParams.get("scope"); - let response_mode = authorizationRequestUrl.searchParams.get("response_mode"); - let nonce = authorizationRequestUrl.searchParams.get("nonce"); - let state = authorizationRequestUrl.searchParams.get("state") as string | null; - let request_uri = authorizationRequestUrl.searchParams.get("request_uri") as string | null; - const request = authorizationRequestUrl.searchParams.get("request"); - - - try { - if(request) { - let requestPayload: any; - try { - requestPayload = JSON.parse(base64url.decode(request.split('.')[1])); - } - catch(error) { - throw new Error(`Invalid Request parameter: Request is not a jwt. Details: ${error}`); - } - - if(requestPayload.response_type && requestPayload.response_type !== response_type) { - throw new Error('Request JWT response_type and authorization request response_type search param do not match'); - } - - if(requestPayload.scope && requestPayload.scope !== scope) { - throw new Error('Request JWT scope and authorization request scope search param do not match'); - } - - if(requestPayload.client_id && requestPayload.client_id !== client_id) { - throw new Error('Request JWT client_id and authorization request client_id search param do not match'); - } - - if(requestPayload.response_uri && requestPayload.response_uri !== response_uri) { - throw new Error('Request JWT redirect_uri and authorization request redirect_uri search param do not match'); - } - - if(requestPayload.response_mode) - response_mode = requestPayload.response_mode; - - if(requestPayload.nonce) - nonce = requestPayload.nonce - } - } - catch(error) { - throw new Error(`Error decoding request search parameter: ${error}`); - } - - let presentation_definition: PresentationDefinition | null; - try { - presentation_definition = await this.fetchPresentationDefinition(authorizationRequestUrl); - } - catch(error) { - console.error(`Error fetching Presentation Definition: ${error}`); - } - - // Finally, check if all required variables have been given - - if(response_type !== "vp_token" && response_type !== "id_token") { - console.error(`Expected response_type = vp_token or id_token, got ${response_type}`); - throw new Error('Invalid response type'); - } - - if(client_id === null) { - throw new Error('Client ID not given'); - } - - if(response_uri === null) { - throw new Error('response_uri not given'); - } - - if(scope !== "openid") { - console.error(`Expected scope = openid, got ${scope}`); - throw new Error('Invalid scope'); - } - - if(response_mode !== "direct_post") { - console.error(`Expected response_mode = direct_post, got ${response_mode}`); - throw new Error('Invalid response mode'); - } - - if(nonce === null) { - throw new Error('Nonce not given'); - } - - // if(!presentation_definition) { - // throw new Error('Presentation Definition not given'); - // } - - return { - client_id, - response_type, - scope, - response_uri, - response_mode, - nonce, - presentation_definition, - state, - request_uri - } - - } - } diff --git a/src/services/SocketManagerService.ts b/src/services/SocketManagerService.ts index 148e2fc..2e017b0 100644 --- a/src/services/SocketManagerService.ts +++ b/src/services/SocketManagerService.ts @@ -1,12 +1,12 @@ import { injectable } from "inversify"; import { ExpectingSocketMessageErr, SocketManagerServiceInterface } from "./interfaces"; -import { Application } from "express"; import * as WebSocket from 'ws'; import http from 'http'; import { Err, Ok, Result } from "ts-results"; import { ServerSocketMessage, ClientSocketMessage, SignatureAction } from "./shared.types"; import { jwtVerify } from "jose"; import config from "../../config"; +import { UserId } from "../entities/user.entity"; const openSockets = new Map(); @@ -34,7 +34,7 @@ export class SocketManagerService implements SocketManagerServiceInterface { return; } const { payload } = await jwtVerify(appToken, secret); - openSockets.set(payload.did as string, ws); + openSockets.set(payload.uuid as string, ws); ws.send(JSON.stringify({ type: "FIN_INIT" })); console.log("Handshake established"); } @@ -48,14 +48,14 @@ export class SocketManagerService implements SocketManagerServiceInterface { }); } - async send(userDid: string, message: ServerSocketMessage): Promise> { - const ws = openSockets.get(userDid); + async send(userId: UserId, message: ServerSocketMessage): Promise> { + const ws = openSockets.get(userId.id); ws.send(JSON.stringify(message)); return Ok.EMPTY; } - async expect(userDid: string, message_id: string, action: SignatureAction): Promise> { - const ws = openSockets.get(userDid); + async expect(userId: UserId, message_id: string, action: SignatureAction): Promise> { + const ws = openSockets.get(userId.id); return new Promise((resolve, reject) => { ws.onmessage = event => { try { @@ -79,4 +79,4 @@ export class SocketManagerService implements SocketManagerServiceInterface { -} \ No newline at end of file +} diff --git a/src/services/VerifierRegistryService.ts b/src/services/VerifierRegistryService.ts index 6f3f2c2..76d5267 100644 --- a/src/services/VerifierRegistryService.ts +++ b/src/services/VerifierRegistryService.ts @@ -59,4 +59,4 @@ export class VerifierRegistryService { async getAllVerifiers() { return this.verifierRegistry; } -} \ No newline at end of file +} diff --git a/src/services/W3CDidKeyUtilityService.ts b/src/services/W3CDidKeyUtilityService.ts index a68445a..27e6d85 100644 --- a/src/services/W3CDidKeyUtilityService.ts +++ b/src/services/W3CDidKeyUtilityService.ts @@ -38,4 +38,4 @@ export class W3CDidKeyUtilityService implements DidKeyUtilityService { return { did: didDocument.id, key: key }; } -} \ No newline at end of file +} diff --git a/src/services/WalletKeystoreManagerService.ts b/src/services/WalletKeystoreManagerService.ts index 59030b7..97d26e6 100644 --- a/src/services/WalletKeystoreManagerService.ts +++ b/src/services/WalletKeystoreManagerService.ts @@ -3,7 +3,7 @@ import { AdditionalKeystoreParameters, DidKeyUtilityService, RegistrationParams, import { Err, Ok, Result } from "ts-results"; import 'reflect-metadata'; import { TYPES } from "./types"; -import { WalletType, getUserByDID } from "../entities/user.entity"; +import { UserId, WalletType, getUser } from "../entities/user.entity"; /** * This class is responsible for deciding which WalletKeystore will be used each time depending on the user @@ -17,15 +17,14 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { @inject(TYPES.DidKeyUtilityService) private didKeyUtilityService: DidKeyUtilityService ) { } - async initializeWallet(registrationParams: RegistrationParams): Promise> { + async initializeWallet(registrationParams: RegistrationParams): Promise> { const fcmToken = registrationParams.fcm_token ? registrationParams.fcm_token : ""; // depending on additionalParameters, decide to use the corresponding keystore service - if (registrationParams.keys && registrationParams.privateData) { + if (registrationParams.privateData) { return Ok({ fcmToken, - keys: Buffer.from(JSON.stringify(registrationParams.keys)), - did: registrationParams.keys.did, + keys: Buffer.from(""), displayName: registrationParams.displayName, privateData: Buffer.from(registrationParams.privateData), walletType: WalletType.CLIENT @@ -34,11 +33,10 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { else { try { console.log("Regular database") - const { did, key } = await this.didKeyUtilityService.generateKeyPair(); + const { key } = await this.didKeyUtilityService.generateKeyPair(); return Ok({ fcmToken, keys: Buffer.from(JSON.stringify(key)), - did: did, displayName: registrationParams.displayName, privateData: Buffer.from(""), walletType: WalletType.DB @@ -49,41 +47,29 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { } } } - - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters?: AdditionalKeystoreParameters): Promise> { - const userRes = await getUserByDID(userDid) - if (userRes.err) { - return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); - } - const user = userRes.unwrap(); - if (user.walletType != WalletType.DB) - return await this.clientWalletKeystoreService.createIdToken(userDid, nonce, audience, additionalParameters); - else - return await this.databaseKeystoreService.createIdToken(userDid, nonce, audience, additionalParameters); - } - async signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise> { - const userRes = await getUserByDID(userDid) + async signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise> { + const userRes = await getUser(userId) if (userRes.err) { return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); } const user = userRes.unwrap(); if (user.walletType != WalletType.DB) - return await this.clientWalletKeystoreService.signJwtPresentation(userDid, nonce, audience, verifiableCredentials, additionalParameters); + return await this.clientWalletKeystoreService.signJwtPresentation(userId, nonce, audience, verifiableCredentials, additionalParameters); else - return await this.databaseKeystoreService.signJwtPresentation(userDid, nonce, audience, verifiableCredentials, additionalParameters); + return await this.databaseKeystoreService.signJwtPresentation(userId, nonce, audience, verifiableCredentials, additionalParameters); } - async generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise> { - const userRes = await getUserByDID(userDid) + async generateOpenid4vciProof(userId: UserId, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise> { + const userRes = await getUser(userId) if (userRes.err) { return Err(WalletKeystoreErr.KEYS_UNAVAILABLE); } const user = userRes.unwrap(); if (user.walletType != WalletType.DB) - return await this.clientWalletKeystoreService.generateOpenid4vciProof(userDid, audience, nonce, additionalParameters); + return await this.clientWalletKeystoreService.generateOpenid4vciProof(userId, audience, nonce, additionalParameters); else - return await this.databaseKeystoreService.generateOpenid4vciProof(userDid, audience, nonce, additionalParameters); + return await this.databaseKeystoreService.generateOpenid4vciProof(userId, audience, nonce, additionalParameters); } -} \ No newline at end of file +} diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 43211c7..65e62cd 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -5,17 +5,14 @@ import { OutboundRequest } from "./types/OutboundRequest"; import http from 'http'; import { WalletKeystoreRequest, ServerSocketMessage, SignatureAction, ClientSocketMessage } from "./shared.types"; import { WalletKey } from "@wwwallet/ssi-sdk"; -import { WalletType } from "../entities/user.entity"; +import { UserId, WalletType } from "../entities/user.entity"; export interface OpenidCredentialReceiving { - - getAvailableSupportedCredentials(userDid: string, legalPersonIdentifier: string): Promise> - generateAuthorizationRequestURL(userDid: string, credentialOfferURL?: string, legalPersonIdentifier?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }>; - - handleAuthorizationResponse(userDid: string, authorizationResponseURL: string): Promise>; - requestCredentialsWithPreAuthorizedGrant(userDid: string, user_pin: string): Promise<{error?: string}>; - - getIssuerState(userDid: string): Promise<{ issuer_state?: string, error?: Error }> + + generateAuthorizationRequestURL(userId: UserId, credentialOfferURL?: string, legalPersonIdentifier?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }>; + + handleAuthorizationResponse(userId: UserId, authorizationResponseURL: string): Promise>; + requestCredentialsWithPreAuthorizedGrant(userId: UserId, user_pin: string): Promise<{error?: string}>; } export enum IssuanceErr { @@ -38,24 +35,22 @@ export type AdditionalKeystoreParameters = { export type RegistrationParams = { fcm_token?: string; keys?: WalletKey; - privateData?: any; + privateData?: Buffer; displayName: string; } export interface WalletKeystoreManager { - initializeWallet(registrationParams: RegistrationParams): Promise>; + initializeWallet(registrationParams: RegistrationParams): Promise>; - createIdToken(userDid: string, nonce: string, audience: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; - signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise>; - generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; + signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise>; + generateOpenid4vciProof(userId: UserId, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; } export interface WalletKeystore { - createIdToken(userDid: string, nonce: string, audience: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; - signJwtPresentation(userDid: string, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise>; - generateOpenid4vciProof(userDid: string, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; + signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise>; + generateOpenid4vciProof(userId: UserId, audience: string, nonce: string, additionalParameters?: AdditionalKeystoreParameters): Promise>; } export enum WalletKeystoreErr { @@ -68,17 +63,9 @@ export enum WalletKeystoreErr { export interface OutboundCommunication { - initiateVerificationFlow(username: string, verifierId: number, scopeName: string): Promise<{ redirect_to?: string }>; - - handleRequest(userDid: string, requestURL: string, camera_was_used: boolean): Promise>; - - /** - * - * @param userDid - * @param req - * @param selection (key: descriptor_id, value: verifiable credential identifier) - */ - sendResponse(userDid: string, selection: Map): Promise>; + initiateVerificationFlow(userId: UserId, verifierId: number, scopeName: string): Promise<{ redirect_to?: string }>; + handleRequest(userId: UserId, requestURL: string, camera_was_used: boolean): Promise>; + sendResponse(userId: UserId, selection: Map): Promise>; } @@ -103,6 +90,6 @@ export enum ExpectingSocketMessageErr { export interface SocketManagerServiceInterface { register(server: http.Server); - send(userDid: string, message: ServerSocketMessage): Promise>; - expect(userDid: string, message_id: string, action: SignatureAction): Promise>; -} \ No newline at end of file + send(userId: UserId, message: ServerSocketMessage): Promise>; + expect(userId: UserId, message_id: string, action: SignatureAction): Promise>; +} diff --git a/src/services/inversify.config.ts b/src/services/inversify.config.ts index 7cfb0e8..ef5e049 100644 --- a/src/services/inversify.config.ts +++ b/src/services/inversify.config.ts @@ -60,4 +60,4 @@ appContainer.bind(TYPES.VerifierRegistryService) appContainer.bind(TYPES.SocketManagerService) .to(SocketManagerService) -export { appContainer } \ No newline at end of file +export { appContainer } diff --git a/src/services/shared.types.ts b/src/services/shared.types.ts index 817a37a..6df67fc 100644 --- a/src/services/shared.types.ts +++ b/src/services/shared.types.ts @@ -1,12 +1,10 @@ export enum SignatureAction { generateOpenid4vciProof = "generateOpenid4vciProof", - createIdToken = "createIdToken", signJwtPresentation = "signJwtPresentation" } export type WalletKeystoreRequest = ( { action: SignatureAction.generateOpenid4vciProof, audience: string, nonce: string } - | { action: SignatureAction.createIdToken, nonce: string, audience: string } | { action: SignatureAction.signJwtPresentation, nonce: string, audience: string, verifiableCredentials: any[] } ); @@ -20,7 +18,6 @@ export type ServerSocketMessage = { export type WalletKeystoreResponse = ( { action: SignatureAction.generateOpenid4vciProof, proof_jwt: string } - | { action: SignatureAction.createIdToken, id_token: string } | { action: SignatureAction.signJwtPresentation, vpjwt: string } ); diff --git a/src/services/types.ts b/src/services/types.ts index 96c8bb0..d282639 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -21,4 +21,4 @@ const TYPES = { }; -export { TYPES }; \ No newline at end of file +export { TYPES }; diff --git a/src/types/errors/user.errors.ts b/src/types/errors/user.errors.ts index 3faf957..f677817 100644 --- a/src/types/errors/user.errors.ts +++ b/src/types/errors/user.errors.ts @@ -2,4 +2,4 @@ export type FetchUserErrors = 'NOT_FOUND' | 'DB_ERROR'; export type RegisterUserErrors = 'ALREADY_EXISTS' | "FILESYSTEM_ERROR"; -export type StoreVcErrors = 'DB_ERROR'; \ No newline at end of file +export type StoreVcErrors = 'DB_ERROR'; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 8004696..6d3171e 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -4,9 +4,9 @@ import { AppTokenUser } from "../middlewares/auth.middleware"; export {} declare global { - namespace Express { - export interface Request { - user?: AppTokenUser; - } - } -} \ No newline at end of file + namespace Express { + export interface Request { + user?: AppTokenUser; + } + } +} diff --git a/src/types/oid4vci/index.ts b/src/types/oid4vci/index.ts index 4a25fa8..af7a550 100644 --- a/src/types/oid4vci/index.ts +++ b/src/types/oid4vci/index.ts @@ -1,2 +1,2 @@ export * from "./oid4vci.types"; -export * from "./oid4vci.zod"; \ No newline at end of file +export * from "./oid4vci.zod"; diff --git a/src/types/oid4vci/oid4vci.types.ts b/src/types/oid4vci/oid4vci.types.ts index 97a424d..86f8f1b 100644 --- a/src/types/oid4vci/oid4vci.types.ts +++ b/src/types/oid4vci/oid4vci.types.ts @@ -29,7 +29,7 @@ export type CredentialOffer = { }, "urn:ietf:params:oauth:grant-type:pre-authorized_code"?: { "pre-authorized_code": string, - "user_pin_required": boolean + "user_pin_required": boolean } } } @@ -38,7 +38,7 @@ export type CredentialOffer = { export type CredentialOfferCredential = { format: VerifiableCredentialFormat, - types: string[] // VerifiableCredential, UniversityDegreeCredential + types: string[] // VerifiableCredential, UniversityDegreeCredential } export type CredentialIssuerMetadata = { @@ -73,7 +73,7 @@ export type CredentialSupportedBase = { cryptographic_binding_methods_supported?: string[], cryptographic_suites_supported?: string[], display?: Display[] -} +} // additional attributes for credentials_supported object for the 'jwt_vc_json' format specifically // https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-objects-comprising-credenti @@ -98,36 +98,36 @@ export type ProofHeader = { alg: string; /** - * CONDITIONAL. JWT header containing the key ID. - * If the credential shall be bound to a DID, the kid refers to a DID URL which identifies a particular key in the DID Document that the credential shall be bound to. - */ + * CONDITIONAL. JWT header containing the key ID. + * If the credential shall be bound to a DID, the kid refers to a DID URL which identifies a particular key in the DID Document that the credential shall be bound to. + */ kid?: string; /** - * CONDITIONAL. JWT header containing the key material the new credential shall be bound to. MUST NOT be present if kid is present. - * REQUIRED for EBSI DID Method for Natural Persons. - */ + * CONDITIONAL. JWT header containing the key material the new credential shall be bound to. MUST NOT be present if kid is present. + * REQUIRED for EBSI DID Method for Natural Persons. + */ jwk?: JWK; } export type ProofPayload = { /** - * REQUIRED. MUST contain the client_id of the sender. - * in DID format - */ + * REQUIRED. MUST contain the client_id of the sender. + * in DID format + */ iss: string; /** - * REQUIRED. MUST contain the issuer URL of the credential issuer. - */ + * REQUIRED. MUST contain the issuer URL of the credential issuer. + */ aud: string; iat: number; /** - * REQUIRED. MUST be Token Response c_nonce as provided by the issuer. - */ + * REQUIRED. MUST be Token Response c_nonce as provided by the issuer. + */ nonce: string; } @@ -141,4 +141,4 @@ export enum VerifiableCredentialFormat { export enum ProofType { JWT = "jwt" -} \ No newline at end of file +} diff --git a/src/util/util.ts b/src/util/util.ts index 514c22e..ebe4013 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,12 +1,16 @@ - -import * as randomstring from 'randomstring'; import * as crypto from 'crypto'; import base64url from "base64url"; +import { Err, Ok, Result } from 'ts-results'; + + +export function isResult(a: T | Result): a is Result { + return a instanceof Object && "val" in a && "ok" in a && "err" in a; +} /** - * - * @param type is the 'type' attribute of a VC in JSON-LD format - */ +* +* @param type is the 'type' attribute of a VC in JSON-LD format +*/ export function decideVerifiableCredentialType(type: string[]): 'Diploma' | 'Attestation' | 'Presentation' { if (type.includes('VerifiablePresentation')) return 'Presentation'; @@ -15,10 +19,10 @@ export function decideVerifiableCredentialType(type: string[]): 'Diploma' | 'Att for (const t of type) { const lower = t.toLowerCase(); if (lower.includes('europass') || - lower.includes('universitydegree') || - lower.includes('diploma')) { + lower.includes('universitydegree') || + lower.includes('diploma')) { - return 'Diploma'; + return 'Diploma'; } } @@ -26,19 +30,19 @@ export function decideVerifiableCredentialType(type: string[]): 'Diploma' | 'Att } export function jsonStringifyTaggedBinary(value: any): string { - return JSON.stringify(value, replacerBufferToTaggedBase64Url); + return JSON.stringify(value, replacerBufferToTaggedBase64Url); } export function jsonParseTaggedBinary(json: string): any { - return JSON.parse(json, reviverTaggedBase64UrlToBuffer); + return JSON.parse(json, reviverTaggedBase64UrlToBuffer); } export function replacerBufferToTaggedBase64Url(key: string, value: any): any { - if (this[key] instanceof Buffer) { - return { '$b64u': base64url.encode(this[key]) }; - } else { - return value; - } + if (this[key] instanceof Buffer) { + return { '$b64u': base64url.encode(this[key]) }; + } else { + return value; + } } export function reviverTaggedBase64UrlToBuffer(key: string, value: any): any { @@ -49,6 +53,41 @@ export function reviverTaggedBase64UrlToBuffer(key: string, value: any): any { } } + +export type EtagUpdate = { + expectTag: string, + newValue: T, +} + +/** + * Return `newValue` if and only if `comparator` returns a value strictly equal + * (`===`) to `expectTag` given `currentValue`. + */ +export function checkedUpdate( + expectTag: U, + tagFunc: (value: T) => U, + { currentValue, newValue }: { currentValue: T, newValue: T }, +): Result { + if (currentValue === newValue) { + // Change has already been applied (if T supports === equality) + return Ok(newValue); + + } else { + const currentTag = tagFunc(currentValue) + if (currentTag === expectTag) { + // Expected change + return Ok(newValue); + + } else { + if (currentTag === tagFunc(newValue)) { + // Change has already been applied (if T does not support === equality) + return Ok(newValue); + } + } + return Err.EMPTY; + } +} + export function isValidUri(uri: string): boolean { try { return Boolean(new URL(uri)); diff --git a/src/webauthn.ts b/src/webauthn.ts index b4ebb3a..3a8461c 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,7 +1,11 @@ import config from '../config'; -import { WebauthnCredentialEntity } from './entities/user.entity'; +import { UserId, WebauthnCredentialEntity } from './entities/user.entity'; +export function getRpId(): string { + return config.webauthn.rp.id; +} + export function makeCreateOptions({ challenge, prfSalt, @@ -10,7 +14,7 @@ export function makeCreateOptions({ challenge: Buffer, prfSalt?: Buffer, user: { - webauthnUserHandle: string, + uuid: UserId, name: string, displayName: string, webauthnCredentials?: WebauthnCredentialEntity[], @@ -20,7 +24,7 @@ export function makeCreateOptions({ publicKey: { rp: config.webauthn.rp, user: { - id: Buffer.from(user.webauthnUserHandle, "utf8"), + id: user.uuid.asUserHandle(), name: user.name, displayName: user.displayName, }, @@ -60,7 +64,7 @@ export function makeGetOptions({ }) { return { publicKey: { - rpId: config.webauthn.rp.id, + rpId: getRpId(), challenge: challenge, allowCredentials: (user?.webauthnCredentials || []).map(cred => cred.getCredentialDescriptor()), userVerification: "required",