From 64be462364de02be8420a3123ec9a80a1d5e99e0 Mon Sep 17 00:00:00 2001 From: gregory1996 Date: Tue, 25 Jun 2024 13:38:38 +0300 Subject: [PATCH 01/51] Send all necessary user data after success signup --- src/routers/user.router.ts | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 057c4c9..7fff879 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -7,7 +7,7 @@ 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 { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, newWebauthnCredentialEntity, WebauthnCredentialEntity, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; import { jsonParseTaggedBinary, jsonStringifyTaggedBinary } from '../util/util'; import { AuthMiddleware } from '../middlewares/auth.middleware'; import { ChallengeErr, createChallenge, popChallenge } from '../entities/WebauthnChallenge.entity'; @@ -50,6 +50,22 @@ async function initSession(user: UserEntity): Promise<{ did: string, appToken: s }; } +async function filterUserData(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string, webauthnCredentials: WebauthnCredentialEntity[] }> { + const secret = new TextEncoder().encode(config.appSecret); + const appToken = await new SignJWT({ did: user.did }) + .setProtectedHeader({ alg: "HS256" }) + .sign(secret); + return { + id: user.id, + appToken, + did: user.did, + displayName: user.displayName || user.username, + privateData: user.privateData.toString(), + username: user.username, + webauthnUserHandle: user.webauthnUserHandle, + webauthnCredentials: user.webauthnCredentials, + }; +} noAuthUserController.post('/register', async (req: Request, res: Response) => { const username = req.body.username; @@ -60,7 +76,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) { @@ -105,7 +121,7 @@ noAuthUserController.post('/register/db-keys', async (req: Request, res: Respons }) noAuthUserController.post('/login/db-keys', async (req: Request, res: Response) => { - + }) noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: Response) => { @@ -162,9 +178,9 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: 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 }) } @@ -187,10 +203,13 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: ], }; - 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).send({ + session: await initSession(userRes.val), + newUser: await filterUserData(userRes.val) + }); } else { res.status(500).send({}); } @@ -277,8 +296,8 @@ userController.post('/fcm_token/add', async (req: Request, res: Response) => { const userDID = req.user.did; updateUserByDID(userDID, (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; From b7fe54a7b8afadcf88f1fb7ce81cf73555a8d1e1 Mon Sep 17 00:00:00 2001 From: gregory1996 Date: Tue, 25 Jun 2024 15:38:56 +0300 Subject: [PATCH 02/51] Fix response on login-webauthn --- src/routers/user.router.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 7fff879..5b518d2 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -281,7 +281,10 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re }); if (updateCredentialRes.ok) { - res.status(200).send(await initSession(user)); + res.status(200).send({ + session: await initSession(user), + newUser: await filterUserData(user) + }); } else { res.status(500).send({}); } From b39f272559e514588130b2655568f5760cc85ae7 Mon Sep 17 00:00:00 2001 From: gregory1996 Date: Thu, 4 Jul 2024 11:26:33 +0300 Subject: [PATCH 03/51] add to init session the id of the user --- src/routers/user.router.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 5b518d2..9c0c8a5 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -36,12 +36,13 @@ userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{ did: string, appToken: string, username?: string, displayName: string, privateData: string }> { +async function initSession(user: UserEntity): Promise<{id: number; 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); return { + id:user.id, appToken, did: user.did, displayName: user.displayName || user.username, From 05c3e42d83a3c10be93db975ed3c03abe0c7759f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 25 Mar 2024 16:18:23 +0100 Subject: [PATCH 04/51] Use replacerBufferToTaggedBase64Url globally --- src/app.ts | 3 ++- src/routers/user.router.ts | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/app.ts b/src/app.ts index f7fe265..9da959d 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,6 +27,7 @@ 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" diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 057c4c9..de0225d 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -8,7 +8,7 @@ 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 { jsonParseTaggedBinary } from '../util/util'; import { AuthMiddleware } from '../middlewares/auth.middleware'; import { ChallengeErr, createChallenge, popChallenge } from '../entities/WebauthnChallenge.entity'; import * as webauthn from '../webauthn'; @@ -125,10 +125,10 @@ noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: }, }); - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ challengeId: challenge.id, createOptions, - })); + }); }); noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: Response) => { @@ -208,10 +208,10 @@ 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) => { @@ -301,7 +301,7 @@ userController.get('/account-info', async (req: Request, res: Response) => { const keys = jsonParseTaggedBinary(user.keys.toString()); - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ username: user.username, displayName: user.displayName, did: user.did, @@ -316,7 +316,7 @@ 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) => { @@ -349,11 +349,11 @@ 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) => { @@ -410,9 +410,9 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo }); if (updateUserRes.ok) { - res.status(200).send(jsonStringifyTaggedBinary({ + res.status(200).send({ credentialId: credential.id - })); + }); } else { res.status(500).send({}); } From 31a421d38d037c69091f967c201f561db904fe22 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 25 Mar 2024 16:20:42 +0100 Subject: [PATCH 05/51] Change type of privateData from string to Buffer --- src/routers/user.router.ts | 8 ++++---- src/services/interfaces.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index de0225d..29e4861 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -36,7 +36,7 @@ userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{ did: string, appToken: string, username?: string, displayName: string, privateData: string }> { +async function initSession(user: UserEntity): Promise<{ did: string, appToken: string, username?: string, displayName: string, privateData: Buffer }> { const secret = new TextEncoder().encode(config.appSecret); const appToken = await new SignJWT({ did: user.did }) .setProtectedHeader({ alg: "HS256" }) @@ -45,7 +45,7 @@ async function initSession(user: UserEntity): Promise<{ did: string, appToken: s appToken, did: user.did, displayName: user.displayName || user.username, - privateData: user.privateData.toString(), + privateData: user.privateData, username: user.username, }; } @@ -404,7 +404,7 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo }, manager) ); if (req.body.privateData) { - userEntity.privateData = Buffer.from(req.body.privateData); + userEntity.privateData = req.body.privateData; } return userEntity; }); @@ -453,7 +453,7 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: } const user = userRes.unwrap(); - const deleteRes = await deleteWebauthnCredential(user, req.params.id, Buffer.from(req.body.privateData)); + const deleteRes = await deleteWebauthnCredential(user, req.params.id, req.body.privateData); if (deleteRes.ok) { res.status(204).send(); } else { diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 43211c7..1e42038 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -38,7 +38,7 @@ export type AdditionalKeystoreParameters = { export type RegistrationParams = { fcm_token?: string; keys?: WalletKey; - privateData?: any; + privateData?: Buffer; displayName: string; } From a2fbc24a15ab25e2d84a4831e516643abe6133c1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 22 Mar 2024 13:52:39 +0100 Subject: [PATCH 06/51] Add route to update user's private data --- src/entities/user.entity.ts | 1 + src/routers/user.router.ts | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 768a7b6..373b0ba 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -146,6 +146,7 @@ enum UpdateUserErr { NOT_EXISTS = "NOT_EXISTS", DB_ERR = "DB_ERR", LAST_WEBAUTHN_CREDENTIAL = "LAST_WEBAUTHN_CREDENTIAL", + CONFLICT = "CONFLICT", } enum UpdateFcmError { diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 29e4861..7f6cbac 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -469,6 +469,29 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: } }) +userController.post('/update-private-data', async (req: Request, res: Response) => { + console.log("update private data", req.body); + + const updateUserRes = await updateUserByDID(req.user.did, userEntity => { + userEntity.privateData = req.body; + return userEntity; + }); + + if (updateUserRes.ok) { + res.status(204).send(); + } else { + if (updateUserRes.val === UpdateUserErr.NOT_EXISTS) { + res.status(404).send(); + + } else if (updateUserRes.val === UpdateUserErr.CONFLICT) { + res.status(409).send(); + + } else { + res.status(500).send(); + } + } +}) + userController.delete('/', async (req: Request, res: Response) => { const userDID = req.user.did; try { From b58e64eb7eb3e02c7b5adb97975b7147ed227764 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 21 Feb 2024 13:12:37 +0100 Subject: [PATCH 07/51] Copy .editorconfig and GitHub Actions workflow from wallet-frontend --- .editorconfig | 13 +++++++++++++ .gitattributes | 8 ++++++++ .github/workflows/code-formatting.yml | 26 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/workflows/code-formatting.yml 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 From 845f29d513cca91c8dbcffb92d38325a7171fa48 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 18 Jul 2024 19:42:51 +0200 Subject: [PATCH 08/51] Fix insert_final_newline violations --- .buildignore | 2 +- .vscode/settings.json | 2 +- Dockerfile | 2 +- cli/.dockerignore | 2 +- cli/Dockerfile | 2 +- config/config.template.ts | 2 +- config/index.ts | 2 +- nodemon.json | 2 +- samples/wallet-mock/config.js | 2 +- samples/wallet-mock/development.Dockerfile | 2 +- samples/wallet-mock/lib.js | 2 +- samples/wallet-mock/nodemon.json | 2 +- samples/wallet-mock/public/javascripts/index.js | 2 +- samples/wallet-mock/public/stylesheets/devtools.css | 2 +- samples/wallet-mock/public/stylesheets/style.css | 2 +- samples/wallet-mock/views/index.pug | 2 +- samples/wallet-mock/views/presentations.pug | 2 +- samples/wallet-mock/views/select-vc.pug | 2 +- samples/wallet-mock/views/vc.pug | 2 +- src/dto/issuance.dto.ts | 2 +- src/dto/user.dto.ts | 2 +- src/dto/verification.dto.ts | 2 +- src/entities/FcmToken.entity.ts | 2 +- src/entities/VerifiableCredential.entity.ts | 2 +- src/entities/VerifiablePresentation.entity.ts | 2 +- src/lib/firebase.ts | 2 +- src/lib/leafnodepaths.ts | 2 +- src/routers/legal_person.router.ts | 2 +- src/routers/status.router.ts | 2 +- src/routers/verifiers.router.ts | 2 +- src/services/ClientKeystoreService.ts | 2 +- src/services/EBSIDidKeyUtilityService.ts | 2 +- src/services/SocketManagerService.ts | 2 +- src/services/VerifierRegistryService.ts | 2 +- src/services/W3CDidKeyUtilityService.ts | 2 +- src/services/WalletKeystoreManagerService.ts | 2 +- src/services/interfaces.ts | 2 +- src/services/inversify.config.ts | 2 +- src/services/types.ts | 2 +- src/types/errors/user.errors.ts | 2 +- src/types/index.d.ts | 2 +- src/types/oid4vci/index.ts | 2 +- src/types/oid4vci/oid4vci.types.ts | 2 +- 43 files changed, 43 insertions(+), 43 deletions(-) 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/.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..cf15c72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,4 +23,4 @@ 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/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/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/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..cc831e3 100644 --- a/samples/wallet-mock/public/javascripts/index.js +++ b/samples/wallet-mock/public/javascripts/index.js @@ -1709,4 +1709,4 @@ } } },{"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..2ec5edf 100644 --- a/samples/wallet-mock/public/stylesheets/style.css +++ b/samples/wallet-mock/public/stylesheets/style.css @@ -11,4 +11,4 @@ a { .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/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..f0fc417 100644 --- a/src/entities/FcmToken.entity.ts +++ b/src/entities/FcmToken.entity.ts @@ -36,4 +36,4 @@ async function deleteAllFcmTokensForUser(did: string, options?: { entityManager? export { deleteAllFcmTokensForUser -} \ No newline at end of file +} 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..a7796f5 100644 --- a/src/entities/VerifiablePresentation.entity.ts +++ b/src/entities/VerifiablePresentation.entity.ts @@ -204,4 +204,4 @@ export { deletePresentationsByCredentialId, getPresentationByIdentifier, deleteAllPresentationsWithHolderDID -} \ No newline at end of file +} diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 308ae69..3bd4550 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -62,4 +62,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..99e3107 100644 --- a/src/lib/leafnodepaths.ts +++ b/src/lib/leafnodepaths.ts @@ -31,4 +31,4 @@ export function getLeafNodesWithPath(verifiableCredential, obj, path = "$.creden console.log("Leafnode paths = ", leafNodesWithPath) // Group leaf nodes by path return leafNodesWithPath -} \ No newline at end of file +} 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/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..afd122d 100644 --- a/src/services/ClientKeystoreService.ts +++ b/src/services/ClientKeystoreService.ts @@ -93,4 +93,4 @@ export class ClientKeystoreService implements WalletKeystore { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } -} \ No newline at end of file +} 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/SocketManagerService.ts b/src/services/SocketManagerService.ts index 148e2fc..9beacb6 100644 --- a/src/services/SocketManagerService.ts +++ b/src/services/SocketManagerService.ts @@ -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..b64f5f2 100644 --- a/src/services/WalletKeystoreManagerService.ts +++ b/src/services/WalletKeystoreManagerService.ts @@ -86,4 +86,4 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { return await this.databaseKeystoreService.generateOpenid4vciProof(userDid, audience, nonce, additionalParameters); } -} \ No newline at end of file +} diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 43211c7..75ec917 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -105,4 +105,4 @@ export interface SocketManagerServiceInterface { send(userDid: string, message: ServerSocketMessage): Promise>; expect(userDid: string, message_id: string, action: SignatureAction): Promise>; -} \ No newline at end of file +} 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/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..d6651ea 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -9,4 +9,4 @@ declare global { user?: AppTokenUser; } } -} \ No newline at end of file +} 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..9ec4e15 100644 --- a/src/types/oid4vci/oid4vci.types.ts +++ b/src/types/oid4vci/oid4vci.types.ts @@ -141,4 +141,4 @@ export enum VerifiableCredentialFormat { export enum ProofType { JWT = "jwt" -} \ No newline at end of file +} From 5c7f74409dcfe73f01b7ed1bfe20cd2eb49b9e7a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 18 Jul 2024 19:44:34 +0200 Subject: [PATCH 09/51] Fix trim_trailing_whitespace violations --- README.md | 6 +- public/alt-vc-logo.png | Bin 144230 -> 0 bytes samples/wallet-mock/app.js | 24 +- .../wallet-mock/public/javascripts/index.js | 610 +++++++++--------- src/app.ts | 1 - src/entities/FcmToken.entity.ts | 2 +- src/entities/LegalPerson.entity.ts | 18 +- src/entities/VerifiablePresentation.entity.ts | 6 +- src/lib/leafnodepaths.ts | 6 +- src/middlewares/auth.middleware.ts | 2 +- src/routers/communicationHandler.router.ts | 12 +- src/routers/storage.router.ts | 6 +- src/routers/user.router.ts | 4 +- src/services/DatabaseKeystoreService.ts | 2 +- .../OpenidForCredentialIssuanceService.ts | 48 +- src/services/OpenidForPresentationService.ts | 32 +- src/services/WalletKeystoreManagerService.ts | 2 +- src/services/interfaces.ts | 8 +- src/types/oid4vci/oid4vci.types.ts | 8 +- src/util/util.ts | 2 +- 20 files changed, 398 insertions(+), 401 deletions(-) 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/public/alt-vc-logo.png b/public/alt-vc-logo.png index bc04d5ddd2893d64c2b862dd2695a3e71b4c482b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 144230 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 diff --git a/samples/wallet-mock/app.js b/samples/wallet-mock/app.js index 92fff73..20e94dc 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}` }} ); @@ -173,9 +173,9 @@ app.get('/init/verification/vid', async (req, res) => { /** * For OpenID 4 VCI (Issuance) - * @param {*} req - * @param {*} res - * @param {*} next + * @param {*} req + * @param {*} res + * @param {*} next */ async function handleCredentialOffer(req, res, next) { const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; @@ -193,9 +193,9 @@ async function handleCredentialOffer(req, res, next) { /** * For OpenID 4 VCI (Issuance) - * @param {*} req - * @param {*} res - * @param {*} next + * @param {*} req + * @param {*} res + * @param {*} next */ async function handleAuthorizationResponse(req, res, next) { const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; @@ -215,9 +215,9 @@ async function handleAuthorizationResponse(req, res, next) { /** * For OpenID 4 VP (Verification) - * @param {*} req - * @param {*} res - * @param {*} next + * @param {*} req + * @param {*} res + * @param {*} next */ async function handleAuthorizationRequest(req, res, next) { const url = `${req.protocol}://${req.hostname}${req.originalUrl}`; diff --git a/samples/wallet-mock/public/javascripts/index.js b/samples/wallet-mock/public/javascripts/index.js index cc831e3..711583e 100644 --- a/samples/wallet-mock/public/javascripts/index.js +++ b/samples/wallet-mock/public/javascripts/index.js @@ -2,18 +2,18 @@ /** * Created by r1ch4 on 02/10/2016. */ - + var JSONView = require('json-view'); - + const vc_list = document.getElementById('vc').value; var view = new JSONView('Payload', JSON.parse(vc_list)); - + // view.on('change', function(key, oldValue, newValue){ // console.log('change', key, oldValue, '=>', 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,8 +650,8 @@ } return debugs[set]; }; - - + + /** * Echos the value of a value. Trys to print the value out * in the best way possible given the different types. @@ -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,11 +716,11 @@ // "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'; @@ -728,24 +728,24 @@ 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,13 +852,13 @@ 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'); @@ -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] }; @@ -957,11 +957,11 @@ name = ctx.stylize(name, 'string'); } } - + return name + ': ' + str; } - - + + function reduceToSingleString(output, base, braces) { var numLinesEst = 0; var length = output.reduce(function(prev, cur) { @@ -969,7 +969,7 @@ 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 ') + @@ -978,79 +978,79 @@ ' ' + 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' || @@ -1060,22 +1060,22 @@ 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,34 +1675,34 @@ // 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}); diff --git a/src/app.ts b/src/app.ts index f7fe265..88c175f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -73,4 +73,3 @@ appContainer.get(TYPES.SocketManagerService).regi server.listen(config.port, () => { console.log(`Wallet Backend Server listening with ${config.url}`) }); - diff --git a/src/entities/FcmToken.entity.ts b/src/entities/FcmToken.entity.ts index f0fc417..b5c1276 100644 --- a/src/entities/FcmToken.entity.ts +++ b/src/entities/FcmToken.entity.ts @@ -10,7 +10,7 @@ export class FcmTokenEntity { @Column({ name: "value", type: "varchar", nullable: false }) value: string; - + @ManyToOne(() => UserEntity, (user) => user.fcmTokenList) user: UserEntity; } diff --git a/src/entities/LegalPerson.entity.ts b/src/entities/LegalPerson.entity.ts index 3514224..bad9856 100644 --- a/src/entities/LegalPerson.entity.ts +++ b/src/entities/LegalPerson.entity.ts @@ -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 }) @@ -126,8 +126,8 @@ async function getLegalPersonsBySearchParams(friendlyNameSubstring: string): Pro /** * Will also update the issuer DB entity with the latest metadata - * @param id - * @returns + * @param id + * @returns */ async function getLegalPersonById(id: number): Promise> { @@ -147,8 +147,8 @@ async function getLegalPersonById(id: number): Promise> { @@ -169,8 +169,8 @@ async function getLegalPersonByDID(did: string): Promise> { diff --git a/src/entities/VerifiablePresentation.entity.ts b/src/entities/VerifiablePresentation.entity.ts index a7796f5..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(); diff --git a/src/lib/leafnodepaths.ts b/src/lib/leafnodepaths.ts index 99e3107..d3b7741 100644 --- a/src/lib/leafnodepaths.ts +++ b/src/lib/leafnodepaths.ts @@ -9,7 +9,7 @@ export function getLeafNodesWithPath(verifiableCredential, obj, path = "$.creden 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) @@ -20,8 +20,8 @@ export function getLeafNodesWithPath(verifiableCredential, obj, path = "$.creden else if (typeof obj[key] === "object" && obj[key] !== null) { // Recursively traverse nested objects traverse(obj[key], newPath); - } - + } + } } diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 24f84a0..73fe54c 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -17,7 +17,7 @@ function getCookieDictionary(cookies: any) { const val = cookie.split('=')[1]; cookieDict[key] = val; - + } return cookieDict; } diff --git a/src/routers/communicationHandler.router.ts b/src/routers/communicationHandler.router.ts index 1e268c9..408b347 100644 --- a/src/routers/communicationHandler.router.ts +++ b/src/routers/communicationHandler.router.ts @@ -70,7 +70,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { url, } = req.body; - + if (!(new URL(url).searchParams.get("code"))) { throw new Error("No code was provided"); } @@ -89,7 +89,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { const { user_pin } = req.body; - + const response = await openidForCredentialIssuanceService.requestCredentialsWithPreAuthorizedGrant(req.user.did, user_pin); console.log("Response = ", response) if (response.error) { @@ -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); - + 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/storage.router.ts b/src/routers/storage.router.ts index 552ac88..dca46fd 100644 --- a/src/routers/storage.router.ts +++ b/src/routers/storage.router.ts @@ -35,7 +35,7 @@ async function getAllVerifiableCredentialsController(req, res) { } async function getVerifiableCredentialByCredentialIdentifierController(req, res) { - const holderDID = req.user.did; + const holderDID = req.user.did; const { credential_identifier } = req.params; const vcFetchResult = await getVerifiableCredentialByCredentialIdentifier(holderDID, credential_identifier); if (vcFetchResult.err) { @@ -76,9 +76,9 @@ async function getAllVerifiablePresentationsController(req, res) { } async function getPresentationByPresentationIdentifierController(req, res) { - const holderDID = req.user.did; + 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..991e067 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -105,7 +105,7 @@ noAuthUserController.post('/register/db-keys', async (req: Request, res: Respons }) noAuthUserController.post('/login/db-keys', async (req: Request, res: Response) => { - + }) noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: Response) => { @@ -164,7 +164,7 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const walletInitializationResult = await walletKeystoreManagerService.initializeWallet( {...req.body as RegistrationParams } ); - + if (walletInitializationResult.err) { return res.status(400).send({ error: walletInitializationResult.val }) } diff --git a/src/services/DatabaseKeystoreService.ts b/src/services/DatabaseKeystoreService.ts index db41d51..56f369c 100644 --- a/src/services/DatabaseKeystoreService.ts +++ b/src/services/DatabaseKeystoreService.ts @@ -32,7 +32,7 @@ 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; diff --git a/src/services/OpenidForCredentialIssuanceService.ts b/src/services/OpenidForCredentialIssuanceService.ts index 99de8d1..f55e818 100644 --- a/src/services/OpenidForCredentialIssuanceService.ts +++ b/src/services/OpenidForCredentialIssuanceService.ts @@ -40,7 +40,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei // identifierService: IdentifierService = new IdentifierService(); // legalPersonService: LegalPersonService = new LegalPersonService(); - + // key: userDid public states = new Map(); @@ -58,7 +58,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei 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 }; } @@ -77,10 +77,10 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei } /** - * + * * @param userDid - * @param legalPersonDID - * @returns + * @param legalPersonDID + * @returns * @throws */ async generateAuthorizationRequestURL(userDid: string, credentialOfferURL?: string, legalPersonDID?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }> { @@ -139,7 +139,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") @@ -173,16 +173,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)); @@ -240,7 +240,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei } /** - * + * * @param authorizationResponseURL * @throws */ @@ -279,12 +279,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" }; @@ -364,7 +364,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) => @@ -392,12 +392,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,7 +408,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei }, 2000); }) - + } private async handleCredentialStorage(state: IssuanceState, credentialResponse: CredentialResponseSchemaType) { @@ -423,18 +423,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..878d145 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -110,7 +110,7 @@ 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); @@ -256,8 +256,8 @@ export class OpenidForPresentationService implements OutboundCommunication { /** * @throws * @param userDid - * @param authorizationRequestURL - * @returns + * @param authorizationRequestURL + * @returns */ private async parseAuthorizationRequest(userDid: string, authorizationRequestURL: string): Promise, verifierDomainName: string}, HandleOutboundRequestError>> { console.log("parseAuthorizationRequest userDid = ", userDid) @@ -305,7 +305,7 @@ export class OpenidForPresentationService implements OutboundCommunication { console.log("Definition = ", presentation_definition) - + let descriptors: InputDescriptorType[]; try { descriptors = JSONPath({ @@ -373,12 +373,12 @@ 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> { - + const hasherAndAlgorithm: HasherAndAlgorithm = { hasher: (input: string) => createHash('sha256').update(input).digest(), algorithm: HasherAlgorithm.Sha256 } - + /** * * @param paths example: [ '$.credentialSubject.image', '$.credentialSubject.grade', '$.credentialSubject.val.x' ] @@ -435,9 +435,9 @@ export class OpenidForPresentationService implements OutboundCommunication { else { selectedVCs.push(vcEntity.credential); } - + } - + const fetchedState = this.states.get(userDid); console.log(fetchedState); const { audience, nonce } = fetchedState; @@ -467,11 +467,11 @@ export class OpenidForPresentationService implements OutboundCommunication { 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); @@ -488,9 +488,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++) { @@ -637,7 +637,7 @@ export class OpenidForPresentationService implements OutboundCommunication { console.error(`Error fetching Presentation Definition from URI: ${fetchPresentationDefinitionRes.data}`); throw new Error(`Error fetching Presentation Definition from URI`); } - + return fetchPresentationDefinitionRes.data; } @@ -648,7 +648,7 @@ export class OpenidForPresentationService implements OutboundCommunication { * @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 @@ -672,7 +672,7 @@ export class OpenidForPresentationService implements OutboundCommunication { let request_uri = authorizationRequestUrl.searchParams.get("request_uri") as string | null; const request = authorizationRequestUrl.searchParams.get("request"); - + try { if(request) { let requestPayload: any; @@ -701,7 +701,7 @@ export class OpenidForPresentationService implements OutboundCommunication { if(requestPayload.response_mode) response_mode = requestPayload.response_mode; - + if(requestPayload.nonce) nonce = requestPayload.nonce } diff --git a/src/services/WalletKeystoreManagerService.ts b/src/services/WalletKeystoreManagerService.ts index b64f5f2..f54e3b0 100644 --- a/src/services/WalletKeystoreManagerService.ts +++ b/src/services/WalletKeystoreManagerService.ts @@ -49,7 +49,7 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { } } } - + async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters?: AdditionalKeystoreParameters): Promise> { const userRes = await getUserByDID(userDid) if (userRes.err) { diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 75ec917..b0c83b8 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -8,10 +8,10 @@ import { WalletKey } from "@wwwallet/ssi-sdk"; import { 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}>; @@ -73,9 +73,9 @@ export interface OutboundCommunication { handleRequest(userDid: string, requestURL: string, camera_was_used: boolean): Promise>; /** - * + * * @param userDid - * @param req + * @param req * @param selection (key: descriptor_id, value: verifiable credential identifier) */ sendResponse(userDid: string, selection: Map): Promise>; diff --git a/src/types/oid4vci/oid4vci.types.ts b/src/types/oid4vci/oid4vci.types.ts index 9ec4e15..5973f9e 100644 --- a/src/types/oid4vci/oid4vci.types.ts +++ b/src/types/oid4vci/oid4vci.types.ts @@ -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,13 +98,13 @@ export type ProofHeader = { alg: string; /** - * CONDITIONAL. JWT header containing the key ID. + * 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. + * 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; diff --git a/src/util/util.ts b/src/util/util.ts index 514c22e..baf151a 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -4,7 +4,7 @@ import * as crypto from 'crypto'; import base64url from "base64url"; /** - * + * * @param type is the 'type' attribute of a VC in JSON-LD format */ export function decideVerifiableCredentialType(type: string[]): 'Diploma' | 'Attestation' | 'Presentation' { From 25b3a47a7ccd46a49cc657a5019499eae1a9087c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 18 Jul 2024 19:56:50 +0200 Subject: [PATCH 10/51] Fix indent_style violations --- Dockerfile | 4 +- development.Dockerfile | 4 +- samples/wallet-mock/app.js | 68 +++++++++--------- .../wallet-mock/public/javascripts/index.js | 46 ++++++------ .../wallet-mock/public/stylesheets/style.css | 6 +- src/entities/LegalPerson.entity.ts | 28 ++++---- src/lib/leafnodepaths.ts | 28 ++++---- src/middlewares/auth.middleware.ts | 42 +++++------ src/services/OpenidForPresentationService.ts | 70 +++++++++---------- src/types/index.d.ts | 10 +-- src/types/oid4vci/oid4vci.types.ts | 28 ++++---- src/util/util.ts | 26 +++---- 12 files changed, 180 insertions(+), 180 deletions(-) diff --git a/Dockerfile b/Dockerfile index cf15c72..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,7 +17,7 @@ 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 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/samples/wallet-mock/app.js b/samples/wallet-mock/app.js index 20e94dc..657940d 100644 --- a/samples/wallet-mock/app.js +++ b/samples/wallet-mock/app.js @@ -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/public/javascripts/index.js b/samples/wallet-mock/public/javascripts/index.js index 711583e..703590f 100644 --- a/samples/wallet-mock/public/javascripts/index.js +++ b/samples/wallet-mock/public/javascripts/index.js @@ -1,7 +1,7 @@ (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 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]; @@ -1053,11 +1053,11 @@ 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; diff --git a/samples/wallet-mock/public/stylesheets/style.css b/samples/wallet-mock/public/stylesheets/style.css index 2ec5edf..62e2131 100644 --- a/samples/wallet-mock/public/stylesheets/style.css +++ b/samples/wallet-mock/public/stylesheets/style.css @@ -1,10 +1,10 @@ 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; } diff --git a/src/entities/LegalPerson.entity.ts b/src/entities/LegalPerson.entity.ts index bad9856..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 }) @@ -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/lib/leafnodepaths.ts b/src/lib/leafnodepaths.ts index d3b7741..785f7a6 100644 --- a/src/lib/leafnodepaths.ts +++ b/src/lib/leafnodepaths.ts @@ -1,13 +1,13 @@ 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 @@ -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 } diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 73fe54c..75338a4 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -10,46 +10,46 @@ export type AppTokenUser = { } function getCookieDictionary(cookies: any) { - const cookieList = cookies.split('; '); - let cookieDict: any = {}; - for (const cookie of cookieList) { - const key = cookie.split('=')[0] as string; + 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; + const val = cookie.split('=')[1]; + cookieDict[key] = val; - } - return cookieDict; + } + return cookieDict; } async function verifyApptoken(jwt: string): Promise<{valid: boolean, payload: any}> { 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); + return { valid: true, payload: payload }; + } + catch (err) { + console.log('Signature verification failed'); + return { valid: false, payload: {}} + } } export function AuthMiddleware(req: Request, res: Response, next: NextFunction) { let token: string; const authorizationHeader = req.headers.authorization; console.log("Authorization header = ", authorizationHeader) - if (req.headers != undefined && authorizationHeader != undefined) { + if (req.headers != undefined && authorizationHeader != undefined) { if (authorizationHeader.split(' ')[0] !== 'Bearer') { res.status(401).send(); return; } token = authorizationHeader.split(' ')[1]; - } - else { + } + else { console.log("Unauthorized access to token: ", authorizationHeader?.split(' ')[1]); res.status(401).send(); // Unauthorized - return; - } + return; + } verifyApptoken(token).then(async ({valid, payload}) => { if (valid === false) { diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index 878d145..705d0e3 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -254,19 +254,19 @@ export class OpenidForPresentationService implements OutboundCommunication { } /** - * @throws - * @param userDid - * @param authorizationRequestURL - * @returns - */ + * @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; + 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; @@ -370,8 +370,8 @@ export class OpenidForPresentationService implements OutboundCommunication { /** - * selection: (key: descriptor_id, value: credentialIdentifier from VerifiableCredential DB entity) - */ + * selection: (key: descriptor_id, value: credentialIdentifier from VerifiableCredential DB entity) + */ private async generateVerifiablePresentation(selection: Map, presentation_definition: PresentationDefinition, userDid: string): Promise> { const hasherAndAlgorithm: HasherAndAlgorithm = { @@ -380,10 +380,10 @@ export class OpenidForPresentationService implements OutboundCommunication { } /** - * - * @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 = {}; @@ -404,9 +404,9 @@ export class OpenidForPresentationService implements OutboundCommunication { return result; }; let vcListRes = await getAllVerifiableCredentials(userDid); - if (vcListRes.err) { - throw "Failed to fetch credentials"; - } + if (vcListRes.err) { + throw "Failed to fetch credentials"; + } const allSelectedCredentialIdentifiers = Array.from(selection.values()); const filteredVCEntities = vcListRes @@ -561,19 +561,19 @@ 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 - */ + * 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; @@ -643,10 +643,10 @@ export class OpenidForPresentationService implements OutboundCommunication { /** - * Handle Authorization Request search Parameters. - * @param authorizationRequest a string of the authorization request URL - * @returns An object containing Authorization Request Parameters - */ + * 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; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index d6651ea..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; - } - } + namespace Express { + export interface Request { + user?: AppTokenUser; + } + } } diff --git a/src/types/oid4vci/oid4vci.types.ts b/src/types/oid4vci/oid4vci.types.ts index 5973f9e..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 } } } @@ -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; } diff --git a/src/util/util.ts b/src/util/util.ts index baf151a..cf6475f 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -4,9 +4,9 @@ import * as crypto from 'crypto'; import base64url from "base64url"; /** - * - * @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 +15,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 +26,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 { From b42c222889ca33e4a73fd23cad1e4aadd1db28ed Mon Sep 17 00:00:00 2001 From: gkatrakazas Date: Mon, 22 Jul 2024 13:07:22 +0300 Subject: [PATCH 11/51] Add scope on sendPushNotification --- src/lib/firebase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 308ae69..d28170e 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: { From db67cd63eaada9c8a7654f51cd473171c30cef78 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 22 Jul 2024 16:30:18 +0200 Subject: [PATCH 12/51] Return WebAuthn RP ID in login success response --- src/routers/user.router.ts | 14 +++++++++++--- src/webauthn.ts | 6 +++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 7f6cbac..0babbaa 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -36,7 +36,14 @@ userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{ did: string, appToken: string, username?: string, displayName: string, privateData: Buffer }> { +async function initSession(user: UserEntity): Promise<{ + did: string, + appToken: string, + username?: string, + displayName: string, + privateData: Buffer, + webauthnRpId: string, +}> { const secret = new TextEncoder().encode(config.appSecret); const appToken = await new SignJWT({ did: user.did }) .setProtectedHeader({ alg: "HS256" }) @@ -47,6 +54,7 @@ async function initSession(user: UserEntity): Promise<{ did: string, appToken: s displayName: user.displayName || user.username, privateData: user.privateData, username: user.username, + webauthnRpId: webauthn.getRpId(), }; } @@ -105,7 +113,7 @@ noAuthUserController.post('/register/db-keys', async (req: Request, res: Respons }) noAuthUserController.post('/login/db-keys', async (req: Request, res: Response) => { - + }) noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: Response) => { @@ -164,7 +172,7 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const walletInitializationResult = await walletKeystoreManagerService.initializeWallet( {...req.body as RegistrationParams } ); - + if (walletInitializationResult.err) { return res.status(400).send({ error: walletInitializationResult.val }) } diff --git a/src/webauthn.ts b/src/webauthn.ts index b4ebb3a..9fbbd5a 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -2,6 +2,10 @@ import config from '../config'; import { WebauthnCredentialEntity } from './entities/user.entity'; +export function getRpId(): string { + return config.webauthn.rp.id; +} + export function makeCreateOptions({ challenge, prfSalt, @@ -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", From dd9f5186868e452e7136606e3b598b1ec9485f44 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 22 Mar 2024 13:50:53 +0100 Subject: [PATCH 13/51] Add type guard isResult --- src/entities/common.entity.ts | 3 ++- src/util/util.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) 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/util/util.ts b/src/util/util.ts index 514c22e..3f08e16 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,7 +1,11 @@ - -import * as randomstring from 'randomstring'; import * as crypto from 'crypto'; import base64url from "base64url"; +import { Result } from 'ts-results'; + + +export function isResult(a: T | Result): a is Result { + return "val" in a && "ok" in a && "err" in a; +} /** * From 234d7f21131d7d942a8b76f55a7da71c1a904a62 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 22 Mar 2024 13:45:02 +0100 Subject: [PATCH 14/51] Explicitly abort updateUserByDID transaction on exceptions --- src/entities/user.entity.ts | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 373b0ba..86e9a33 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -6,6 +6,7 @@ import base64url from "base64url"; import AppDataSource from "../AppDataSource"; import * as scrypt from "../scrypt"; import { FcmTokenEntity } from "./FcmToken.entity"; +import { isResult } from "../util/util"; export enum WalletType { DB, @@ -348,26 +349,30 @@ function newWebauthnCredentialEntity(data: DeepPartial } 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 + try { + return await userRepository.manager.transaction(async (manager) => { + const res = await manager.findOne(UserEntity, { + where: { + did: did + } + }); + if (!res) { + return Promise.reject(Err(UpdateUserErr.NOT_EXISTS)); } - }); - if (!res) { - return Err(UpdateUserErr.NOT_EXISTS); - } - - const updatedUser = update(res, manager); - try { + const updatedUser = update(res, manager); await manager.save(updatedUser); - return Ok(res); - } catch (e) { + }); + } catch (e) { + if (isResult(e)) { + if (e.err) { + return e as Result; + } + } else { console.log(e); return Err(UpdateUserErr.DB_ERR); } - }); + } } async function updateWebauthnCredentialWithManager( From ab57e6d954937f7cbcb89fd489fa51eefe2d307b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 22 Mar 2024 17:33:06 +0100 Subject: [PATCH 15/51] Return void instead of {} from deleteWebauthnCredential --- src/entities/user.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 86e9a33..88d0dac 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -414,7 +414,7 @@ async function updateWebauthnCredentialById(userDid: string, credentialUuid: str }); } -async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, newPrivateData: Buffer): Promise> { +async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, newPrivateData: Buffer): Promise> { try { return await userRepository.manager.transaction(async (manager) => { @@ -439,7 +439,7 @@ async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string .execute(); if (res.affected > 0) { await manager.update(UserEntity, { did: user.did }, { privateData: newPrivateData }); - return Ok({}); + return Ok.EMPTY; } else if (res.affected === 0) { return Err(UpdateUserErr.NOT_EXISTS); } From 9a450091a8db9f266f2cb694ba7c17fd4954f4ab Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 22 Mar 2024 17:33:21 +0100 Subject: [PATCH 16/51] Use runTransaction in deleteWebauthnCredential --- src/entities/user.entity.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 88d0dac..aba0582 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -7,6 +7,7 @@ import AppDataSource from "../AppDataSource"; import * as scrypt from "../scrypt"; import { FcmTokenEntity } from "./FcmToken.entity"; import { isResult } from "../util/util"; +import { runTransaction } from "./common.entity"; export enum WalletType { DB, @@ -417,7 +418,7 @@ async function updateWebauthnCredentialById(userDid: string, credentialUuid: str async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, newPrivateData: Buffer): Promise> { try { - return await userRepository.manager.transaction(async (manager) => { + return Ok(await runTransaction(async (manager) => { const userRes = await manager.findOne(UserEntity, { where: { did: user.did }}); if (!userRes) { return Err(UpdateUserErr.NOT_EXISTS); @@ -443,7 +444,7 @@ async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string } else if (res.affected === 0) { return Err(UpdateUserErr.NOT_EXISTS); } - }); + })); } catch (e) { console.log(e); From d17b09595466dcf5bfb6e470c6cdbbed512f9004 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 25 Mar 2024 16:40:31 +0100 Subject: [PATCH 17/51] Abort updateUserByDID transaction if update function returns Err --- src/entities/user.entity.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index aba0582..0d421eb 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -349,7 +349,7 @@ function newWebauthnCredentialEntity(data: DeepPartial return entity; } -async function updateUserByDID(did: string, update: (user: UserEntity, entityManager: EntityManager) => UserEntity): Promise> { +async function updateUserByDID(did: string, update: (user: UserEntity, entityManager: EntityManager) => UserEntity | Result): Promise> { try { return await userRepository.manager.transaction(async (manager) => { const res = await manager.findOne(UserEntity, { @@ -362,12 +362,22 @@ async function updateUserByDID(did: string, update: (user: UserEntity, entityMan } const updatedUser = update(res, manager); - await manager.save(updatedUser); + 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; + return e as Result; } } else { console.log(e); From ff4f5506bf438ea9294d9ce3654d18b6ba064351 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 24 Jul 2024 19:17:20 +0200 Subject: [PATCH 18/51] Re-throw the given UpdateUserErr in deleteWebauthnCredential Instead of collapsing all possible errors down to just `DB_ERR`. --- src/entities/user.entity.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 0d421eb..d9dc736 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -457,8 +457,8 @@ async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string })); } catch (e) { - console.log(e); - return Err(UpdateUserErr.DB_ERR); + console.log('Failed to delete WebAuthn credential:', e); + return Err(e); } } From 7f55c8e243fbeb0a9083ff7004d8015349bf6a95 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Wed, 24 Jul 2024 19:13:52 +0200 Subject: [PATCH 19/51] Protect UserEntity.privateData update with ETag/If-Match headers This helps prevent data loss if a user has two concurrent sessions and one attempts to overwrite changes made by the other. Each client must now keep track of the `X-Private-Data-ETag` value returned when the client most recently updated its local copy of the private data, and send that value as the `X-Private-Data-If-Match` header when performing an update. If the header is missing or does not match the current server state, the update is rejected. These headers are meant to imitate the general-purpose `ETag` and `If-Match` headers, but named with a prefix to indicate that they only apply to the private data field when used in requests that also retrieve or handle other fields. --- src/app.ts | 7 ++- src/entities/user.entity.ts | 32 +++++++++-- src/routers/user.router.ts | 110 ++++++++++++++++++++++++++++-------- src/util/util.ts | 24 +++++++- 4 files changed, 143 insertions(+), 30 deletions(-) diff --git a/src/app.ts b/src/app.ts index 9da959d..02a38e8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -32,7 +32,12 @@ 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 diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index d9dc736..7f77a8d 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -6,7 +6,7 @@ import base64url from "base64url"; import AppDataSource from "../AppDataSource"; import * as scrypt from "../scrypt"; import { FcmTokenEntity } from "./FcmToken.entity"; -import { isResult } from "../util/util"; +import { checkedUpdate, EtagUpdate, isResult } from "../util/util"; import { runTransaction } from "./common.entity"; export enum WalletType { @@ -14,6 +14,16 @@ export enum WalletType { 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}"`; +} + + @Entity({ name: "user" }) class UserEntity { @PrimaryGeneratedColumn() @@ -148,7 +158,7 @@ enum UpdateUserErr { NOT_EXISTS = "NOT_EXISTS", DB_ERR = "DB_ERR", LAST_WEBAUTHN_CREDENTIAL = "LAST_WEBAUTHN_CREDENTIAL", - CONFLICT = "CONFLICT", + PRIVATE_DATA_CONFLICT = "PRIVATE_DATA_CONFLICT", } enum UpdateFcmError { @@ -425,9 +435,8 @@ 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 Ok(await runTransaction(async (manager) => { const userRes = await manager.findOne(UserEntity, { where: { did: user.did }}); if (!userRes) { @@ -449,8 +458,19 @@ 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.EMPTY; + const newPrivateData = checkedUpdate( + updatePrivateData.expectTag, + privateDataEtag, + { + currentValue: userRes.privateData, + newValue: updatePrivateData.newValue, + }); + if (newPrivateData.ok) { + await manager.update(UserEntity, { did: user.did }, { privateData: newPrivateData.val }); + return Ok.EMPTY; + } else { + return Err(UpdateUserErr.PRIVATE_DATA_CONFLICT); + } } else if (res.affected === 0) { return Err(UpdateUserErr.NOT_EXISTS); } diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 0babbaa..734aaa6 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -7,8 +7,8 @@ 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 } from '../util/util'; +import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; +import { checkedUpdate, EtagUpdate, jsonParseTaggedBinary } from '../util/util'; import { AuthMiddleware } from '../middlewares/auth.middleware'; import { ChallengeErr, createChallenge, popChallenge } from '../entities/WebauthnChallenge.entity'; import * as webauthn from '../webauthn'; @@ -85,7 +85,9 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { 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") @@ -106,7 +108,9 @@ 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) => { @@ -198,7 +202,9 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: 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({}); } @@ -270,7 +276,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({}); } @@ -411,16 +419,32 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo prfCapable: credential.clientExtensionResults?.prf?.enabled || false, }, manager) ); - if (req.body.privateData) { - userEntity.privateData = 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({ - credentialId: credential.id - }); + res.status(200) + .header({ 'X-Private-Data-ETag': privateDataEtag(user.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({}); } @@ -461,9 +485,15 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: } const user = userRes.unwrap(); - const deleteRes = await deleteWebauthnCredential(user, req.params.id, 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(user.privateData) }) + .send(); } else { if (deleteRes.val === UpdateUserErr.NOT_EXISTS) { res.status(404).send(); @@ -471,34 +501,70 @@ 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(user.privateData) }) + .send(); + } else { res.status(500).send(); } } }) -userController.post('/update-private-data', async (req: Request, res: Response) => { - console.log("update private data", req.body); - +userController.post('/private-data', async (req: Request, res: Response) => { const updateUserRes = await updateUserByDID(req.user.did, userEntity => { - userEntity.privateData = req.body; - return 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).send(); + 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 === UpdateUserErr.CONFLICT) { - res.status(409).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 getUserByDID(req.user.did); + 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; diff --git a/src/util/util.ts b/src/util/util.ts index 3f08e16..311a37f 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -1,6 +1,6 @@ import * as crypto from 'crypto'; import base64url from "base64url"; -import { Result } from 'ts-results'; +import { Err, Ok, Result } from 'ts-results'; export function isResult(a: T | Result): a is Result { @@ -53,6 +53,28 @@ 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 (tagFunc(currentValue) === expectTag) { + return Ok(newValue); + } else { + return Err.EMPTY; + } +} + export function isValidUri(uri: string): boolean { try { return Boolean(new URL(uri)); From aa114fa334579c4cf4e484dbd3a0be38a8a677e3 Mon Sep 17 00:00:00 2001 From: gkatrakazas Date: Thu, 25 Jul 2024 12:47:28 +0300 Subject: [PATCH 20/51] Fix Trailing whitespace --- src/routers/user.router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 9c0c8a5..ca555bd 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -36,13 +36,13 @@ userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{id: number; did: string, appToken: string, username?: string, displayName: string, privateData: string }> { +async function initSession(user: UserEntity): Promise<{ id: number; 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); return { - id:user.id, + id: user.id, appToken, did: user.did, displayName: user.displayName || user.username, @@ -285,7 +285,7 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re res.status(200).send({ session: await initSession(user), newUser: await filterUserData(user) - }); + }); } else { res.status(500).send({}); } From 860edbe11ed339c6313343c605d4e112f19aa223 Mon Sep 17 00:00:00 2001 From: gkatrakazas Date: Mon, 29 Jul 2024 11:32:10 +0300 Subject: [PATCH 21/51] Remove initSession and use only filterUserData --- src/routers/user.router.ts | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index ca555bd..6f8b2a8 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -35,22 +35,6 @@ const userController: Router = express.Router(); userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); - -async function initSession(user: UserEntity): Promise<{ id: number; 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); - return { - id: user.id, - appToken, - did: user.did, - displayName: user.displayName || user.username, - privateData: user.privateData.toString(), - username: user.username, - }; -} - async function filterUserData(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string, webauthnCredentials: WebauthnCredentialEntity[] }> { const secret = new TextEncoder().encode(config.appSecret); const appToken = await new SignJWT({ did: user.did }) @@ -94,7 +78,7 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { const result = (await createUser(newUser)); if (result.ok) { - res.status(200).send(await initSession(result.val)); + res.status(200).send(await filterUserData(result.val)); } else { console.log("Failed to create user") @@ -115,7 +99,7 @@ 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).send(await filterUserData(user)); }) noAuthUserController.post('/register/db-keys', async (req: Request, res: Response) => { @@ -207,10 +191,7 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const userRes = await createUser(newUser, false,); if (userRes.ok) { console.log("Created user", userRes.val); - res.status(200).send({ - session: await initSession(userRes.val), - newUser: await filterUserData(userRes.val) - }); + res.status(200).send(await filterUserData(userRes.val)); } else { res.status(500).send({}); } @@ -282,10 +263,7 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re }); if (updateCredentialRes.ok) { - res.status(200).send({ - session: await initSession(user), - newUser: await filterUserData(user) - }); + res.status(200).send(await filterUserData(user)); } else { res.status(500).send({}); } From 4d7162fd5ba08afe582eeb3e83389f5cbfd0629f Mon Sep 17 00:00:00 2001 From: gkatrakazas Date: Mon, 29 Jul 2024 11:51:32 +0300 Subject: [PATCH 22/51] Rename filterUserData to initSession --- src/routers/user.router.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 6f8b2a8..b48ebf0 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -35,7 +35,7 @@ const userController: Router = express.Router(); userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function filterUserData(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string, webauthnCredentials: WebauthnCredentialEntity[] }> { +async function initSession(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string, webauthnCredentials: WebauthnCredentialEntity[] }> { const secret = new TextEncoder().encode(config.appSecret); const appToken = await new SignJWT({ did: user.did }) .setProtectedHeader({ alg: "HS256" }) @@ -78,7 +78,7 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { const result = (await createUser(newUser)); if (result.ok) { - res.status(200).send(await filterUserData(result.val)); + res.status(200).send(await initSession(result.val)); } else { console.log("Failed to create user") @@ -99,7 +99,7 @@ noAuthUserController.post('/login', async (req: Request, res: Response) => { } console.log('user res = ', userRes) const user = userRes.unwrap(); - res.status(200).send(await filterUserData(user)); + res.status(200).send(await initSession(user)); }) noAuthUserController.post('/register/db-keys', async (req: Request, res: Response) => { @@ -191,7 +191,7 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const userRes = await createUser(newUser, false,); if (userRes.ok) { console.log("Created user", userRes.val); - res.status(200).send(await filterUserData(userRes.val)); + res.status(200).send(await initSession(userRes.val)); } else { res.status(500).send({}); } @@ -263,7 +263,7 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re }); if (updateCredentialRes.ok) { - res.status(200).send(await filterUserData(user)); + res.status(200).send(await initSession(user)); } else { res.status(500).send({}); } From 5016d6e27d874ce15b15c271ce1d9435f1a11b1e Mon Sep 17 00:00:00 2001 From: gkatrakazas Date: Mon, 29 Jul 2024 11:58:52 +0300 Subject: [PATCH 23/51] Drop webauthnCredentials from initSession --- src/routers/user.router.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index b48ebf0..608cb62 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -7,7 +7,7 @@ import base64url from 'base64url'; import { EntityManager } from "typeorm" import config from '../../config'; -import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, newWebauthnCredentialEntity, WebauthnCredentialEntity, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; +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 { ChallengeErr, createChallenge, popChallenge } from '../entities/WebauthnChallenge.entity'; @@ -35,7 +35,7 @@ const userController: Router = express.Router(); userController.use(AuthMiddleware); noAuthUserController.use('/session', userController); -async function initSession(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string, webauthnCredentials: WebauthnCredentialEntity[] }> { +async function initSession(user: UserEntity): Promise<{ id: number, did: string, appToken: string, username?: string, displayName: string, privateData: string, webauthnUserHandle: string }> { const secret = new TextEncoder().encode(config.appSecret); const appToken = await new SignJWT({ did: user.did }) .setProtectedHeader({ alg: "HS256" }) @@ -48,7 +48,6 @@ async function initSession(user: UserEntity): Promise<{ id: number, did: string, privateData: user.privateData.toString(), username: user.username, webauthnUserHandle: user.webauthnUserHandle, - webauthnCredentials: user.webauthnCredentials, }; } From 13aef0e3d1d1e7c6ea087a6405fabb88792abae1 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 29 Jul 2024 13:02:12 +0200 Subject: [PATCH 24/51] Remove unused UpdateUserErr.CONFLICT --- src/entities/user.entity.ts | 1 - src/routers/user.router.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 373b0ba..768a7b6 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -146,7 +146,6 @@ enum UpdateUserErr { NOT_EXISTS = "NOT_EXISTS", DB_ERR = "DB_ERR", LAST_WEBAUTHN_CREDENTIAL = "LAST_WEBAUTHN_CREDENTIAL", - CONFLICT = "CONFLICT", } enum UpdateFcmError { diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index dddbd01..8b3658a 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -494,9 +494,6 @@ userController.post('/update-private-data', async (req: Request, res: Response) if (updateUserRes.val === UpdateUserErr.NOT_EXISTS) { res.status(404).send(); - } else if (updateUserRes.val === UpdateUserErr.CONFLICT) { - res.status(409).send(); - } else { res.status(500).send(); } From a0703cee28f40115b0f068f2aa17cf59ce087a9a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 29 Jul 2024 17:28:13 +0200 Subject: [PATCH 25/51] Fix which privateData value is used to compute response ETag --- src/routers/user.router.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 6305659..6fb44ae 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -442,7 +442,7 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo if (updateUserRes.ok) { res.status(200) - .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .header({ 'X-Private-Data-ETag': privateDataEtag(updateUserRes.val.privateData) }) .send({ credentialId: credential.id }); } else if (updateUserRes.val === UpdateUserErr.PRIVATE_DATA_CONFLICT) { res.status(412) @@ -495,7 +495,7 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: const deleteRes = await deleteWebauthnCredential(user, req.params.id, updatePrivateData); if (deleteRes.ok) { res.status(204) - .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .header({ 'X-Private-Data-ETag': privateDataEtag(updatePrivateData.newValue) }) .send(); } else { if (deleteRes.val === UpdateUserErr.NOT_EXISTS) { @@ -506,7 +506,7 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: } else if (deleteRes.val === UpdateUserErr.PRIVATE_DATA_CONFLICT) { res.status(412) - .header({ 'X-Private-Data-ETag': privateDataEtag(user.privateData) }) + .header({ 'X-Private-Data-ETag': privateDataEtag(updatePrivateData.newValue) }) .send(); } else { From 3e4a9925106c3de97ffdad1505e36209e1f2900b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 29 Jul 2024 17:46:26 +0200 Subject: [PATCH 26/51] Accept already-applied changes in checkedUpdate As suggested in RFC 9110: >[...] Alternatively, if the request is a state-changing operation that appears to have already been applied to the selected representation, the origin server MAY respond with a 2xx (Successful) status code (i.e., the change requested by the user agent has already succeeded, but the user agent might not be aware of it, perhaps because the prior response was lost or an equivalent change was made by some other user agent). --- src/util/util.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/util/util.ts b/src/util/util.ts index 1e058a4..a9318ef 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -68,9 +68,22 @@ export function checkedUpdate( tagFunc: (value: T) => U, { currentValue, newValue }: { currentValue: T, newValue: T }, ): Result { - if (tagFunc(currentValue) === expectTag) { + 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; } } From 5023c42b53ec7de0334aa0eafbb3dd361caca841 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 Aug 2024 17:08:12 +0200 Subject: [PATCH 27/51] Move additional base64 encoding of WebAuthn responses to backend We already have JSON body parsers configured to automatically wrap and unwrap binary data, so we don't need this encoding for the frontend's sake. That the backend uses SimpleWebauthn which expects inputs base64url encoded is an implementation detail that should not leak into the API exposed to the frontend. --- src/routers/user.router.ts | 44 +++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 6fb44ae..1bccdaf 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -163,7 +163,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, @@ -189,14 +198,14 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: webauthnUserHandle, webauthnCredentials: [ newWebauthnCredentialEntity({ - credentialId: Buffer.from(verification.registrationInfo.credentialID), + credentialId: credential.rawId, userHandle: Buffer.from(webauthnUserHandle), 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, }), ], @@ -235,8 +244,8 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re 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 userHandle = credential.response.userHandle.toString(); + const credentialId = credential.rawId; const userRes = await getUserByWebauthnCredential(userHandle, credentialId); if (userRes.err) { @@ -259,7 +268,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, @@ -400,7 +419,16 @@ 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, From 463add28d9ef529720e4fb7ecec6af8abd56174a Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Fri, 2 Aug 2024 18:25:10 +0200 Subject: [PATCH 28/51] Add update: false flag to readonly database columns --- src/entities/WebauthnChallenge.entity.ts | 2 +- src/entities/user.entity.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/entities/WebauthnChallenge.entity.ts b/src/entities/WebauthnChallenge.entity.ts index 01be7ca..20311ff 100644 --- a/src/entities/WebauthnChallenge.entity.ts +++ b/src/entities/WebauthnChallenge.entity.ts @@ -11,7 +11,7 @@ class WebauthnChallengeEntity { type: string; // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 - @Column({ nullable: true, default: () => "NULL" }) + @Column({ nullable: true, default: () => "NULL", update: false }) userHandle?: string; @Column({ nullable: false }) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 7f77a8d..23efb55 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -56,7 +56,7 @@ class UserEntity { @Column({ type: "blob", nullable: false }) privateData: Buffer; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) @Generated("uuid") webauthnUserHandle: string; @@ -82,23 +82,23 @@ class WebauthnCredentialEntity { @ManyToOne(() => UserEntity, (user) => user.webauthnCredentials, { nullable: false }) user: UserEntity; - @Column({ nullable: false }) + @Column({ nullable: false, update: false }) credentialId: Buffer; - @Column({ nullable: false }) + @Column({ nullable: 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 }) @@ -107,10 +107,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 }) From 292e33a7ab2093517ac4a87ba2cd47abf1324106 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 16:31:28 +0200 Subject: [PATCH 29/51] Delete unused function storeKeypair --- src/entities/user.entity.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 23efb55..e3826db 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -193,23 +193,6 @@ 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> { try { const res = await userRepository.findOne({ From 5d5cbb3c14ca30f9baab6da81c5174f6d5ff28ee Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 16:31:59 +0200 Subject: [PATCH 30/51] Delete commented-out code --- src/app.ts | 15 --------------- src/entities/user.entity.ts | 19 ------------------- src/routers/user.router.ts | 20 -------------------- 3 files changed, 54 deletions(-) diff --git a/src/app.ts b/src/app.ts index bbf0b37..52ee73e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -43,21 +43,6 @@ app.use(cors({ // 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); -// }) diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index e3826db..b5721de 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -315,25 +315,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); diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 1bccdaf..9f5acb3 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -617,25 +617,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; From 33e98301189b5775cf95e6d7bfa95d413cc811ad Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 18:02:27 +0200 Subject: [PATCH 31/51] Fix type error in isResult --- src/util/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/util.ts b/src/util/util.ts index a9318ef..ebe4013 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -4,7 +4,7 @@ import { Err, Ok, Result } from 'ts-results'; export function isResult(a: T | Result): a is Result { - return "val" in a && "ok" in a && "err" in a; + return a instanceof Object && "val" in a && "ok" in a && "err" in a; } /** From e850a2e7cb8a905407c1af1edaaf27ae3cca5239 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 18:21:08 +0200 Subject: [PATCH 32/51] Delete unused function OpenidCredentialReceiving.getAvailableSupportedCredentials --- src/services/OpenidForCredentialIssuanceService.ts | 14 -------------- src/services/interfaces.ts | 1 - 2 files changed, 15 deletions(-) diff --git a/src/services/OpenidForCredentialIssuanceService.ts b/src/services/OpenidForCredentialIssuanceService.ts index f55e818..5d269e2 100644 --- a/src/services/OpenidForCredentialIssuanceService.ts +++ b/src/services/OpenidForCredentialIssuanceService.ts @@ -62,20 +62,6 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei 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 diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 9ed6aec..7f16fbc 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -9,7 +9,6 @@ import { 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>; From 12fb505510ca273e2ea51523e6c57e6c0d27c2aa Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 18:57:29 +0200 Subject: [PATCH 33/51] Add type annotations to storage.router handler function params --- src/routers/storage.router.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/routers/storage.router.ts b/src/routers/storage.router.ts index dca46fd..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,7 +34,7 @@ async function getAllVerifiableCredentialsController(req, res) { } -async function getVerifiableCredentialByCredentialIdentifierController(req, res) { +async function getVerifiableCredentialByCredentialIdentifierController(req: Request, res: Response) { const holderDID = req.user.did; const { credential_identifier } = req.params; const vcFetchResult = await getVerifiableCredentialByCredentialIdentifier(holderDID, credential_identifier); @@ -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,7 +75,7 @@ async function getAllVerifiablePresentationsController(req, res) { res.status(200).send({ vp_list: vp_list }) } -async function getPresentationByPresentationIdentifierController(req, res) { +async function getPresentationByPresentationIdentifierController(req: Request, res: Response) { const holderDID = req.user.did; const { presentation_identifier } = req.params; From f2432aeffac047c932a3eb1ca9627d2f205eda09 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 19:11:26 +0200 Subject: [PATCH 34/51] Delete unused function getCookieDictionary --- src/middlewares/auth.middleware.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 75338a4..062ec8f 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -9,19 +9,6 @@ export type AppTokenUser = { 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; -} - async function verifyApptoken(jwt: string): Promise<{valid: boolean, payload: any}> { const secret = new TextEncoder().encode(config.appSecret); try { From 6aafe209ab700dd0af874dbf951dd664d4d3546e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 19:07:02 +0200 Subject: [PATCH 35/51] Extract function createAppToken to auth.middleware module --- src/middlewares/auth.middleware.ts | 11 +++++++++-- src/routers/user.router.ts | 9 ++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 062ec8f..1f420b3 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -1,14 +1,21 @@ 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 { getUserByDID, UserEntity } from "../entities/user.entity"; export type AppTokenUser = { username: string; did: string; } +export async function createAppToken(user: UserEntity): Promise { + const secret = new TextEncoder().encode(config.appSecret); + return await new SignJWT({ did: user.did }) + .setProtectedHeader({ alg: "HS256" }) + .sign(secret); +} + async function verifyApptoken(jwt: string): Promise<{valid: boolean, payload: any}> { const secret = new TextEncoder().encode(config.appSecret); try { diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 9f5acb3..a64805d 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'; @@ -9,7 +8,7 @@ import { EntityManager } from "typeorm" import config from '../../config'; import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; import { checkedUpdate, EtagUpdate, jsonParseTaggedBinary } from '../util/util'; -import { AuthMiddleware } from '../middlewares/auth.middleware'; +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"; @@ -46,13 +45,9 @@ async function initSession(user: UserEntity): Promise<{ webauthnRpId: string, webauthnUserHandle: string, }> { - const secret = new TextEncoder().encode(config.appSecret); - const appToken = await new SignJWT({ did: user.did }) - .setProtectedHeader({ alg: "HS256" }) - .sign(secret); return { id: user.id, - appToken, + appToken: await createAppToken(user), did: user.did, displayName: user.displayName || user.username, privateData: user.privateData, From f95bc71971f87781e6163c15a6a07699ad9d4467 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:38:40 +0200 Subject: [PATCH 36/51] Merge req.user assignments in AuthMiddleware --- src/middlewares/auth.middleware.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 1f420b3..5fbf124 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -52,20 +52,18 @@ export function AuthMiddleware(req: Request, res: Response, next: NextFunction) return; } - // success - req.user = { - username: "", - did: "" - } as AppTokenUser; - req.user.did = (payload as AppTokenUser).did; - const userRes = await getUserByDID(req.user.did); + const { did } = payload; + const userRes = await getUserByDID(did); if (userRes.err) { res.status(401).send(); // Unauthorized return; } + const user = userRes.unwrap(); - req.user.username = user.username; - req.user.did = user.did; + req.user = { + username: user.username, + did, + }; return next(); }) .catch(e => { From b5e67ddc6226c9dbdc73249a9e7e467518d1f6db Mon Sep 17 00:00:00 2001 From: kkmanos Date: Tue, 6 Aug 2024 18:39:49 +0300 Subject: [PATCH 37/51] removed id-token handled as deprecated --- src/services/OpenidForPresentationService.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index 705d0e3..9844c62 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -112,11 +112,6 @@ export class OpenidForPresentationService implements OutboundCommunication { } async handleRequest(userDid: string, requestURL: string, camera_was_used: boolean): Promise> { - try { - return await this.parseIdTokenRequest(userDid, requestURL); - } - catch(err) { - } try { const url = new URL(requestURL); From 98375bf93f5988c576bcf3460d2bb14582d565de Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:40:14 +0200 Subject: [PATCH 38/51] Invert userRes condition in AuthMiddleware This allows TypeScript to correctly infer the success type of `userRes.val` in the `ok` branch, so we don't need to `unwrap()` it and (seemingly) risk a runtime error. --- src/middlewares/auth.middleware.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 5fbf124..2f35752 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -54,17 +54,16 @@ export function AuthMiddleware(req: Request, res: Response, next: NextFunction) const { did } = payload; const userRes = await getUserByDID(did); - if (userRes.err) { - res.status(401).send(); // Unauthorized - return; + if (userRes.ok) { + req.user = { + username: userRes.val.username, + did, + }; + return next(); } - const user = userRes.unwrap(); - req.user = { - username: user.username, - did, - }; - return next(); + res.status(401).send(); // Unauthorized + return; }) .catch(e => { console.log("Unauthorized access to ", token); From d26a4d4b1b9f0511cf97f0702a289d24fd3ef968 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:47:46 +0200 Subject: [PATCH 39/51] Use type annotations for appToken payload --- src/middlewares/auth.middleware.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 2f35752..324399c 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -4,6 +4,10 @@ import { jwtVerify, SignJWT } from 'jose'; import config from "../../config"; import { getUserByDID, UserEntity } from "../entities/user.entity"; +type AppTokenPayload = { + did: string; +} + export type AppTokenUser = { username: string; did: string; @@ -11,20 +15,21 @@ export type AppTokenUser = { export async function createAppToken(user: UserEntity): Promise { const secret = new TextEncoder().encode(config.appSecret); - return await new SignJWT({ did: user.did }) + const payload: AppTokenPayload = { did: user.did }; + 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 }; + return payload as AppTokenPayload; } catch (err) { console.log('Signature verification failed'); - return { valid: false, payload: {}} + return false; } } @@ -45,8 +50,8 @@ export function AuthMiddleware(req: Request, res: Response, next: NextFunction) return; } - verifyApptoken(token).then(async ({valid, payload}) => { - if (valid === false) { + verifyApptoken(token).then(async (payload) => { + if (!payload) { console.log("Unauthorized access to ", token); res.status(401).send(); // Unauthorized return; From b65daaad2871ca6f59105ab6de388ff5f8e9108f Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:49:40 +0200 Subject: [PATCH 40/51] Eliminate redundant condition --- src/middlewares/auth.middleware.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 324399c..7412a70 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -35,9 +35,9 @@ async function verifyApptoken(jwt: string): Promise { 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 != undefined) { if (authorizationHeader.split(' ')[0] !== 'Bearer') { res.status(401).send(); return; From d100c61fa80dfe94ef2e8f63c03b546b205e31a4 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:56:37 +0200 Subject: [PATCH 41/51] Simplify Authorization header matching logic --- src/middlewares/auth.middleware.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 7412a70..e88b25e 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -34,22 +34,16 @@ async function verifyApptoken(jwt: string): Promise { } export function AuthMiddleware(req: Request, res: Response, next: NextFunction) { - let token: string; const authorizationHeader = req.headers?.authorization; console.log("Authorization header = ", authorizationHeader) - if (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 + if (authorizationHeader?.substring(0, 7) !== 'Bearer ') { + console.log("Invalid authorization header:", authorizationHeader); + res.status(401).send(); return; } + let token: string = authorizationHeader.substring(7); + verifyApptoken(token).then(async (payload) => { if (!payload) { console.log("Unauthorized access to ", token); From 0dfbd7b08bbb24eabccc50e9c289d4ad74f1e81c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 19:43:53 +0200 Subject: [PATCH 42/51] Add version number to token payload --- src/middlewares/auth.middleware.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index e88b25e..579fecf 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -4,7 +4,13 @@ import { jwtVerify, SignJWT } from 'jose'; import config from "../../config"; import { getUserByDID, UserEntity } from "../entities/user.entity"; + +type TokenPayloadVersion = 0; +const TOKEN_PAYLOAD_VERSION: TokenPayloadVersion = 0; + type AppTokenPayload = { + // Increment TokenPayloadVersion whenever AppTokenPayload content changes to invalidate existing tokens + v: TokenPayloadVersion; did: string; } @@ -15,7 +21,10 @@ export type AppTokenUser = { export async function createAppToken(user: UserEntity): Promise { const secret = new TextEncoder().encode(config.appSecret); - const payload: AppTokenPayload = { did: user.did }; + const payload: AppTokenPayload = { + v: TOKEN_PAYLOAD_VERSION, + did: user.did, + }; return await new SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) .sign(secret); @@ -25,7 +34,14 @@ async function verifyApptoken(jwt: string): Promise { const secret = new TextEncoder().encode(config.appSecret); try { const { payload, protectedHeader } = await jwtVerify(jwt, secret); - return payload as AppTokenPayload; + 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'); From 2b6d99585edd236efa3729cc520d27b400b2f21c Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Mon, 5 Aug 2024 19:36:54 +0200 Subject: [PATCH 43/51] Rename UserEntity.webauthnUserHandle to uuid and make primary ID --- src/entities/WebauthnChallenge.entity.ts | 24 +++++-- src/entities/user.entity.ts | 84 +++++++++++++++++++----- src/middlewares/auth.middleware.ts | 4 +- src/routers/user.router.ts | 38 +++++------ src/webauthn.ts | 6 +- 5 files changed, 107 insertions(+), 49 deletions(-) diff --git a/src/entities/WebauthnChallenge.entity.ts b/src/entities/WebauthnChallenge.entity.ts index 20311ff..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", update: false }) - 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/user.entity.ts b/src/entities/user.entity.ts index b5721de..eeed2fe 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,7 +1,8 @@ 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 } 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"; @@ -24,11 +25,63 @@ export function privateDataEtag(privateData: Buffer): string { } +// 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", + transformer: { from: UserId.fromId, to: (userId: UserId) => userId.id }, + }) + @Generated("uuid") + 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" }) @@ -56,10 +109,6 @@ class UserEntity { @Column({ type: "blob", nullable: false }) privateData: Buffer; - @Column({ nullable: false, update: false }) - @Generated("uuid") - webauthnUserHandle: string; - @Column({ type: "enum" ,enum: WalletType, default: WalletType.DB }) walletType: WalletType; @@ -85,8 +134,12 @@ class WebauthnCredentialEntity { @Column({ nullable: false, update: false }) credentialId: Buffer; - @Column({ nullable: false, update: 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" }) @@ -133,14 +186,13 @@ type CreateUser = { passwordHash: string; fcmToken: string; privateData: Buffer; - webauthnUserHandle: string; } | { + uuid: UserId; displayName: string, did: string; keys: Buffer; fcmToken: string; privateData: Buffer; - webauthnUserHandle: string; webauthnCredentials: WebauthnCredentialEntity[]; } @@ -217,7 +269,7 @@ async function deleteUserByDID(did: string, options?: { entityManager: EntityMan const userRes = await manager.findOne(UserEntity, { where: { did: did }}); await manager.delete(WebauthnCredentialEntity, { - user: { id: userRes.id } + user: { uuid: userRes.uuid } }); await manager.delete(UserEntity, { @@ -274,12 +326,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(); diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 579fecf..7912cc5 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { jwtVerify, SignJWT } from 'jose'; import config from "../../config"; -import { getUserByDID, UserEntity } from "../entities/user.entity"; +import { getUserByDID, UserEntity, UserId } from "../entities/user.entity"; type TokenPayloadVersion = 0; @@ -16,6 +16,7 @@ type AppTokenPayload = { export type AppTokenUser = { username: string; + id: UserId; did: string; } @@ -73,6 +74,7 @@ export function AuthMiddleware(req: Request, res: Response, next: NextFunction) req.user = { username: userRes.val.username, did, + id: userRes.val.uuid, }; return next(); } diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index a64805d..92e4f5a 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -6,7 +6,7 @@ import base64url from 'base64url'; import { EntityManager } from "typeorm" import config from '../../config'; -import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity } from '../entities/user.entity'; +import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUserByDID, 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'; @@ -36,24 +36,22 @@ noAuthUserController.use('/session', userController); async function initSession(user: UserEntity): Promise<{ - id: number, + uuid: UserId, did: string, appToken: string, username?: string, displayName: string, privateData: Buffer, webauthnRpId: string, - webauthnUserHandle: string, }> { return { - id: user.id, + uuid: user.uuid, appToken: await createAppToken(user), did: user.did, displayName: user.displayName || user.username, privateData: user.privateData, username: user.username, webauthnRpId: webauthn.getRpId(), - webauthnUserHandle: user.webauthnUserHandle, }; } @@ -78,7 +76,6 @@ noAuthUserController.post('/register', async (req: Request, res: Response) => { ...walletInitializationResult.unwrap(), username: username ? username : "", passwordHash: passwordHash, - webauthnUserHandle: uuid.v4(), }; const result = (await createUser(newUser)); @@ -119,7 +116,8 @@ 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; @@ -129,7 +127,7 @@ noAuthUserController.post('/register-webauthn-begin', async (req: Request, res: const createOptions = webauthn.makeCreateOptions({ challenge: challenge.challenge, user: { - webauthnUserHandle: challenge.userHandle, + uuid: userId, name: "", displayName: "", }, @@ -175,8 +173,7 @@ 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; } @@ -190,11 +187,11 @@ noAuthUserController.post('/register-webauthn-finish', async (req: Request, res: const newUser: CreateUser = { ...walletInitializationResult.unwrap(), - webauthnUserHandle, + uuid: challenge.userId, webauthnCredentials: [ newWebauthnCredentialEntity({ credentialId: credential.rawId, - userHandle: Buffer.from(webauthnUserHandle), + _userHandle: challenge.userId.asUserHandle(), nickname: req.body.nickname, publicKeyCose: Buffer.from(verification.registrationInfo.credentialPublicKey), signatureCount: verification.registrationInfo.counter, @@ -239,10 +236,10 @@ noAuthUserController.post('/login-webauthn-finish', async (req: Request, res: Re console.log("webauthn login-finish", req.body); const credential = req.body.credential; - const userHandle = credential.response.userHandle.toString(); + 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; @@ -335,12 +332,12 @@ userController.get('/account-info', async (req: Request, res: Response) => { const keys = jsonParseTaggedBinary(user.keys.toString()); 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, @@ -353,12 +350,7 @@ userController.get('/account-info', async (req: Request, res: Response) => { }) 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 getUserByDID(req.user.did); if (userRes.err) { res.status(403).send({}); @@ -367,7 +359,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; @@ -435,7 +427,7 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo 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, diff --git a/src/webauthn.ts b/src/webauthn.ts index 9fbbd5a..3a8461c 100644 --- a/src/webauthn.ts +++ b/src/webauthn.ts @@ -1,5 +1,5 @@ import config from '../config'; -import { WebauthnCredentialEntity } from './entities/user.entity'; +import { UserId, WebauthnCredentialEntity } from './entities/user.entity'; export function getRpId(): string { @@ -14,7 +14,7 @@ export function makeCreateOptions({ challenge: Buffer, prfSalt?: Buffer, user: { - webauthnUserHandle: string, + uuid: UserId, name: string, displayName: string, webauthnCredentials?: WebauthnCredentialEntity[], @@ -24,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, }, From eaf5623e0736412941d133df2fc6022bdbff9882 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 6 Aug 2024 17:06:34 +0200 Subject: [PATCH 44/51] Use UserEntity.uuid instead of .did where possible --- src/entities/FcmToken.entity.ts | 8 +- src/entities/user.entity.ts | 57 +++++------- src/middlewares/auth.middleware.ts | 18 ++-- src/routers/communicationHandler.router.ts | 12 +-- src/routers/user.router.ts | 33 +++---- src/services/ClientKeystoreService.ts | 22 ++--- src/services/DatabaseKeystoreService.ts | 14 +-- .../OpenidForCredentialIssuanceService.ts | 70 +++++++-------- src/services/OpenidForPresentationService.ts | 86 +++++++++---------- src/services/SocketManagerService.ts | 12 +-- src/services/WalletKeystoreManagerService.ts | 32 ++++--- src/services/interfaces.ts | 42 ++++----- 12 files changed, 182 insertions(+), 224 deletions(-) diff --git a/src/entities/FcmToken.entity.ts b/src/entities/FcmToken.entity.ts index b5c1276..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"; @@ -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({}); }); diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index eeed2fe..ed20efc 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,5 +1,5 @@ import { Err, Ok, Result } from "ts-results"; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, Repository, EntityManager, DeepPartial, Generated } 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'; @@ -78,9 +78,10 @@ class UserEntity { nullable: false, update: false, name: "webauthnUserHandle", + type: "varchar", + length: 36, transformer: { from: UserId.fromId, to: (userId: UserId) => userId.id }, }) - @Generated("uuid") uuid: UserId; // Explicit default to workaround a bug in typeorm: https://github.com/typeorm/typeorm/issues/3076#issuecomment-703128687 @@ -182,14 +183,12 @@ class WebauthnCredentialEntity { type CreateUser = { username: string; displayName: string, - did: string; passwordHash: string; fcmToken: string; privateData: Buffer; } | { uuid: UserId; displayName: string, - did: string; keys: Buffer; fcmToken: string; privateData: Buffer; @@ -228,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(); @@ -245,13 +247,9 @@ async function createUser(createUser: CreateUser, isAdmin: boolean = false): Pro } } -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); } @@ -263,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: { uuid: userRes.uuid } - }); - - 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({}) }); } @@ -375,14 +366,10 @@ function newWebauthnCredentialEntity(data: DeepPartial return entity; } -async function updateUserByDID(did: string, update: (user: UserEntity, entityManager: EntityManager) => UserEntity | Result): Promise> { +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: { - did: did - } - }); + const res = await manager.findOne(UserEntity, { where: { uuid: Equal(id) } }); if (!res) { return Promise.reject(Err(UpdateUserErr.NOT_EXISTS)); } @@ -436,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(); @@ -454,7 +441,7 @@ async function updateWebauthnCredentialById(userDid: string, credentialUuid: str async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string, updatePrivateData: EtagUpdate): Promise> { try { return Ok(await runTransaction(async (manager) => { - const userRes = await manager.findOne(UserEntity, { where: { did: user.did }}); + const userRes = await manager.findOne(UserEntity, { where: { uuid: Equal(user.uuid) }}); if (!userRes) { return Err(UpdateUserErr.NOT_EXISTS); } @@ -482,7 +469,7 @@ async function deleteWebauthnCredential(user: UserEntity, credentialUuid: string newValue: updatePrivateData.newValue, }); if (newPrivateData.ok) { - await manager.update(UserEntity, { did: user.did }, { privateData: newPrivateData.val }); + await manager.update(UserEntity, { uuid: user.uuid }, { privateData: newPrivateData.val }); return Ok.EMPTY; } else { return Err(UpdateUserErr.PRIVATE_DATA_CONFLICT); @@ -505,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/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts index 7912cc5..624e163 100644 --- a/src/middlewares/auth.middleware.ts +++ b/src/middlewares/auth.middleware.ts @@ -2,16 +2,16 @@ import { Request, Response, NextFunction } from "express"; import { jwtVerify, SignJWT } from 'jose'; import config from "../../config"; -import { getUserByDID, UserEntity, UserId } from "../entities/user.entity"; +import { getUser, UserEntity, UserId } from "../entities/user.entity"; -type TokenPayloadVersion = 0; -const TOKEN_PAYLOAD_VERSION: TokenPayloadVersion = 0; +type TokenPayloadVersion = 1; +const TOKEN_PAYLOAD_VERSION: TokenPayloadVersion = 1; type AppTokenPayload = { // Increment TokenPayloadVersion whenever AppTokenPayload content changes to invalidate existing tokens v: TokenPayloadVersion; - did: string; + uuid: string; } export type AppTokenUser = { @@ -24,7 +24,7 @@ export async function createAppToken(user: UserEntity): Promise { const secret = new TextEncoder().encode(config.appSecret); const payload: AppTokenPayload = { v: TOKEN_PAYLOAD_VERSION, - did: user.did, + uuid: user.uuid.id, }; return await new SignJWT(payload) .setProtectedHeader({ alg: "HS256" }) @@ -68,13 +68,13 @@ export function AuthMiddleware(req: Request, res: Response, next: NextFunction) return; } - const { did } = payload; - const userRes = await getUserByDID(did); + const userId = UserId.fromId(payload.uuid); + const userRes = await getUser(userId); if (userRes.ok) { req.user = { username: userRes.val.username, - did, - id: userRes.val.uuid, + id: userId, + did: userRes.val.did, }; return next(); } diff --git a/src/routers/communicationHandler.router.ts b/src/routers/communicationHandler.router.ts index 408b347..cb982b0 100644 --- a/src/routers/communicationHandler.router.ts +++ b/src/routers/communicationHandler.router.ts @@ -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); } @@ -74,7 +74,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { 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({}); @@ -90,7 +90,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { 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 }); @@ -144,7 +144,7 @@ communicationHandlerRouter.post('/handle', async (req, res) => { 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 }); diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 92e4f5a..66dca87 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -6,7 +6,7 @@ import base64url from 'base64url'; import { EntityManager } from "typeorm" import config from '../../config'; -import { CreateUser, createUser, deleteUserByDID, deleteWebauthnCredential, getUserByCredentials, getUserByDID, getUserByWebauthnCredential, GetUserErr, newWebauthnCredentialEntity, privateDataEtag, updateUserByDID, UpdateUserErr, updateWebauthnCredential, updateWebauthnCredentialById, UserEntity, UserId } from '../entities/user.entity'; +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'; @@ -37,7 +37,6 @@ noAuthUserController.use('/session', userController); async function initSession(user: UserEntity): Promise<{ uuid: UserId, - did: string, appToken: string, username?: string, displayName: string, @@ -47,7 +46,6 @@ async function initSession(user: UserEntity): Promise<{ return { uuid: user.uuid, appToken: await createAppToken(user), - did: user.did, displayName: user.displayName || user.username, privateData: user.privateData, username: user.username, @@ -304,8 +302,7 @@ 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) { @@ -322,7 +319,7 @@ 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; @@ -335,7 +332,6 @@ userController.get('/account-info', async (req: Request, res: Response) => { uuid: user.uuid, username: user.username, displayName: user.displayName, - did: user.did, hasPassword: user.passwordHash !== null, publicKey: keys.publicKey, webauthnCredentials: (user.webauthnCredentials || []).map(cred => ({ @@ -350,7 +346,7 @@ userController.get('/account-info', async (req: Request, res: Response) => { }) userController.post('/webauthn/register-begin', 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({}); @@ -384,7 +380,7 @@ userController.post('/webauthn/register-begin', async (req: Request, res: Respon 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; @@ -422,7 +418,7 @@ userController.post('/webauthn/register-finish', async (req: Request, res: Respo }); 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({ @@ -475,7 +471,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; }); @@ -496,7 +492,7 @@ 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; @@ -531,7 +527,7 @@ userController.post('/webauthn/credential/:id/delete', async (req: Request, res: }) userController.post('/private-data', async (req: Request, res: Response) => { - const updateUserRes = await updateUserByDID(req.user.did, userEntity => { + const updateUserRes = await updateUser(req.user.id, userEntity => { const newPrivateData = checkedUpdate( req.headers['x-private-data-if-match'], privateDataEtag, @@ -568,7 +564,7 @@ userController.post('/private-data', async (req: Request, res: Response) => { }); userController.get('/private-data', async (req: Request, res: Response) => { - const userRes = await getUserByDID(req.user.did); + const userRes = await getUser(req.user.id); if (userRes.ok) { const privateData = userRes.val.privateData; res.status(200) @@ -585,17 +581,16 @@ userController.get('/private-data', async (req: Request, res: Response) => { }); 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 }), ); }); diff --git a/src/services/ClientKeystoreService.ts b/src/services/ClientKeystoreService.ts index afd122d..4c29b2f 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,7 +23,7 @@ export class ClientKeystoreService implements WalletKeystore { ) { } - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { + async createIdToken(userId: UserId, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { let message_id_sent = randomUUID(); const msg = { message_id: message_id_sent, @@ -33,9 +33,9 @@ export class ClientKeystoreService implements WalletKeystore { audience: audience } } - 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.createIdToken); + const result = await this.socketManagerService.expect(userId, message_id_sent, SignatureAction.createIdToken); if (result.err) { return Err(WalletKeystoreErr.REMOTE_SIGNING_FAILED); } @@ -46,7 +46,7 @@ export class ClientKeystoreService implements WalletKeystore { 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 +57,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 +70,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 +81,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); } diff --git a/src/services/DatabaseKeystoreService.ts b/src/services/DatabaseKeystoreService.ts index 56f369c..499254d 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"; @@ -33,8 +33,8 @@ export class DatabaseKeystoreService implements WalletKeystore { } - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { - const user = (await getUserByDID(userDid)).unwrap(); + async createIdToken(userId: UserId, nonce: string, audience: string, additionalParameters: AdditionalKeystoreParameters): Promise> { + const user = (await getUser(userId)).unwrap(); const keys = JSON.parse(user.keys.toString()) as WalletKey; if (!keys.privateKey) { @@ -58,8 +58,8 @@ export class DatabaseKeystoreService implements WalletKeystore { 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 +90,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/OpenidForCredentialIssuanceService.ts b/src/services/OpenidForCredentialIssuanceService.ts index 5d269e2..889efbd 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 @@ -42,7 +38,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei // legalPersonService: LegalPersonService = new LegalPersonService(); - // key: userDid + // key: UserEntity.uuid public states = new Map(); constructor( @@ -50,8 +46,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei ) { } - async getIssuerState(userDid: string): Promise<{ issuer_state?: string, error?: Error; }> { - const state = this.states.get(userDid); + async getIssuerState(userId: UserId): Promise<{ issuer_state?: string, error?: Error; }> { + const state = this.states.get(userId.id); if (!state) { return { issuer_state: null, error: new Error("No state found") }; } @@ -62,15 +58,8 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei return { issuer_state: state.issuer_state, error: null }; } - /** - * - * @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; @@ -113,8 +102,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; @@ -145,8 +135,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, @@ -180,8 +170,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, @@ -196,10 +186,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 => { @@ -208,8 +198,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 {}; @@ -230,8 +220,8 @@ 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); } @@ -246,16 +236,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) } @@ -314,17 +304,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 = { @@ -358,7 +348,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) @@ -398,7 +388,7 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei } private async handleCredentialStorage(state: IssuanceState, credentialResponse: CredentialResponseSchemaType) { - const userRes = await getUserByDID(state.userDid); + const userRes = await getUser(state.userId); if (userRes.err) { return; } diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index 9844c62..00e552d 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -13,7 +13,7 @@ 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,7 +80,7 @@ type VerificationState = { @injectable() export class OpenidForPresentationService implements OutboundCommunication { - // key: did + // key: UserEntity.uuid states = new Map(); @@ -91,15 +90,15 @@ export class OpenidForPresentationService implements OutboundCommunication { @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); @@ -111,7 +110,7 @@ export class OpenidForPresentationService implements OutboundCommunication { return { redirect_to: url.toString() }; } - async handleRequest(userDid: string, requestURL: string, camera_was_used: boolean): Promise> { + async handleRequest(userId: UserId, requestURL: string, camera_was_used: boolean): Promise> { try { const url = new URL(requestURL); @@ -120,8 +119,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); } @@ -140,9 +139,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); @@ -152,8 +151,8 @@ export class OpenidForPresentationService implements OutboundCommunication { - private async parseIdTokenRequest(userDid: string, authorizationRequestURL: string): Promise> { - console.log("parseIdTokenRequest userDid:", userDid) + private async parseIdTokenRequest(userId: UserId, authorizationRequestURL: string): Promise> { + console.log("parseIdTokenRequest userId:", userId) let client_id: string, response_uri: string, @@ -181,25 +180,25 @@ export class OpenidForPresentationService implements OutboundCommunication { } - const currentState = this.states.get(userDid); - this.states.set(userDid, { + const currentState = this.states.get(userId.id); + this.states.set(userId.id, { ...currentState, audience: client_id, nonce, response_uri, state, }); - const idTokenResult = await this.walletKeystoreManagerService.createIdToken(userDid, nonce, client_id); + const idTokenResult = await this.walletKeystoreManagerService.createIdToken(userId, nonce, client_id); if (idTokenResult.ok) { const { id_token } = idTokenResult.val; - return Ok(await this.finishParseIdTokenRequest(userDid, state, response_uri, id_token)); + return Ok(await this.finishParseIdTokenRequest(userId, 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); + private async finishParseIdTokenRequest(userId: UserId, state: string, redirect_uri: string, id_token: string): Promise<{ redirect_to: string }> { + const { issuer_state } = await this.OpenidCredentialReceivingService.getIssuerState(userId); const params = { id_token, @@ -248,15 +247,8 @@ export class OpenidForPresentationService implements OutboundCommunication { 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(); + 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, @@ -285,8 +277,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, @@ -296,7 +288,7 @@ 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) @@ -312,8 +304,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" } @@ -367,7 +361,7 @@ 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> { + private async generateVerifiablePresentation(selection: Map, presentation_definition: PresentationDefinition, userId: UserId): Promise> { const hasherAndAlgorithm: HasherAndAlgorithm = { hasher: (input: string) => createHash('sha256').update(input).digest(), @@ -398,7 +392,8 @@ export class OpenidForPresentationService implements OutboundCommunication { }); return result; }; - let vcListRes = await getAllVerifiableCredentials(userDid); + const user = (await getUser(userId)).unwrap(); + let vcListRes = await getAllVerifiableCredentials(user.did); if (vcListRes.err) { throw "Failed to fetch credentials"; } @@ -433,12 +428,12 @@ export class OpenidForPresentationService implements OutboundCommunication { } - 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, @@ -451,13 +446,14 @@ 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" } @@ -468,14 +464,14 @@ export class OpenidForPresentationService implements OutboundCommunication { 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 }); @@ -544,7 +540,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({ }) } diff --git a/src/services/SocketManagerService.ts b/src/services/SocketManagerService.ts index 9beacb6..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 { diff --git a/src/services/WalletKeystoreManagerService.ts b/src/services/WalletKeystoreManagerService.ts index f54e3b0..519c0a0 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,7 +17,7 @@ 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 @@ -25,7 +25,6 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { return Ok({ fcmToken, keys: Buffer.from(JSON.stringify(registrationParams.keys)), - did: registrationParams.keys.did, 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 @@ -50,40 +48,40 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { } } - async createIdToken(userDid: string, nonce: string, audience: string, additionalParameters?: AdditionalKeystoreParameters): Promise> { - const userRes = await getUserByDID(userDid) + async createIdToken(userId: UserId, nonce: string, audience: 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.createIdToken(userDid, nonce, audience, additionalParameters); + return await this.clientWalletKeystoreService.createIdToken(userId, nonce, audience, additionalParameters); else - return await this.databaseKeystoreService.createIdToken(userDid, nonce, audience, additionalParameters); + return await this.databaseKeystoreService.createIdToken(userId, 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); } } diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 7f16fbc..54d73cd 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -5,16 +5,16 @@ 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 { - generateAuthorizationRequestURL(userDid: string, credentialOfferURL?: string, legalPersonIdentifier?: string): Promise<{ redirect_to?: string, preauth?: boolean, ask_for_pin?: boolean }>; + generateAuthorizationRequestURL(userId: UserId, 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}>; + handleAuthorizationResponse(userId: UserId, authorizationResponseURL: string): Promise>; + requestCredentialsWithPreAuthorizedGrant(userId: UserId, user_pin: string): Promise<{error?: string}>; - getIssuerState(userDid: string): Promise<{ issuer_state?: string, error?: Error }> + getIssuerState(userId: UserId): Promise<{ issuer_state?: string, error?: Error }> } export enum IssuanceErr { @@ -43,18 +43,18 @@ export type RegistrationParams = { 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>; + createIdToken(userId: UserId, nonce: string, audience: 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>; + createIdToken(userId: UserId, nonce: string, audience: 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 { @@ -67,17 +67,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>; } @@ -102,6 +94,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>; + send(userId: UserId, message: ServerSocketMessage): Promise>; + expect(userId: UserId, message_id: string, action: SignatureAction): Promise>; } From 92eca8fbc77d0331fccfa08f1f0fc994e821428e Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Thu, 8 Aug 2024 13:03:17 +0200 Subject: [PATCH 45/51] Don't require keys prop on signup, don't parse it in /account-info --- src/routers/user.router.ts | 3 --- src/services/WalletKeystoreManagerService.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts index 66dca87..82bb1c5 100644 --- a/src/routers/user.router.ts +++ b/src/routers/user.router.ts @@ -326,14 +326,11 @@ userController.get('/account-info', async (req: Request, res: Response) => { } const user = userRes.unwrap(); - const keys = jsonParseTaggedBinary(user.keys.toString()); - res.status(200).send({ uuid: user.uuid, username: user.username, displayName: user.displayName, hasPassword: user.passwordHash !== null, - publicKey: keys.publicKey, webauthnCredentials: (user.webauthnCredentials || []).map(cred => ({ createTime: cred.createTime, credentialId: cred.credentialId, diff --git a/src/services/WalletKeystoreManagerService.ts b/src/services/WalletKeystoreManagerService.ts index 519c0a0..2c3780d 100644 --- a/src/services/WalletKeystoreManagerService.ts +++ b/src/services/WalletKeystoreManagerService.ts @@ -21,10 +21,10 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { 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)), + keys: Buffer.from(""), displayName: registrationParams.displayName, privateData: Buffer.from(registrationParams.privateData), walletType: WalletType.CLIENT From 3902fb42243dcb29f4ceb5a867a201a7ebead286 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:03:19 +0200 Subject: [PATCH 46/51] Delete unused OpenidForPresentationService methods --- src/services/OpenidForPresentationService.ts | 305 +------------------ 1 file changed, 1 insertion(+), 304 deletions(-) diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index 00e552d..bced71b 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -8,7 +8,7 @@ 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, OpenidCredentialReceiving, OutboundCommunication, SendResponseError, WalletKeystore } from "./interfaces"; import { TYPES } from "./types"; import { OutboundRequest } from "./types/OutboundRequest"; import { getAllVerifiableCredentials } from "../entities/VerifiableCredential.entity"; @@ -149,104 +149,6 @@ export class OpenidForPresentationService implements OutboundCommunication { } } - - - private async parseIdTokenRequest(userId: UserId, authorizationRequestURL: string): Promise> { - console.log("parseIdTokenRequest 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(userId.id); - this.states.set(userId.id, { - ...currentState, - audience: client_id, - nonce, - response_uri, - state, - }); - const idTokenResult = await this.walletKeystoreManagerService.createIdToken(userId, nonce, client_id); - if (idTokenResult.ok) { - const { id_token } = idTokenResult.val; - return Ok(await this.finishParseIdTokenRequest(userId, state, response_uri, id_token)); - } else if (idTokenResult.val === WalletKeystoreErr.KEYS_UNAVAILABLE) { - return Err({ action: SignatureAction.createIdToken, nonce, audience: client_id }); - } - } - - private async finishParseIdTokenRequest(userId: UserId, state: string, redirect_uri: string, id_token: string): Promise<{ redirect_to: string }> { - const { issuer_state } = await this.OpenidCredentialReceivingService.getIssuerState(userId); - - 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 } - } - private async parseAuthorizationRequest(userId: UserId, authorizationRequestURL: string): Promise, verifierDomainName: string}, HandleOutboundRequestError>> { console.log("parseAuthorizationRequest userId = ", userId) let client_id: string, @@ -551,209 +453,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 - } - - } - } From 74b53c06a88e52f047906f35001e4c61922bc099 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:05:12 +0200 Subject: [PATCH 47/51] Delete unused OpenidForPresentationService constructor param --- src/services/OpenidForPresentationService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index bced71b..bbbaf37 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -8,7 +8,7 @@ import { z } from 'zod'; import { Err, Ok, Result } from "ts-results"; import { InputDescriptorType, Verify } from "@wwwallet/ssi-sdk"; -import { HandleOutboundRequestError, OpenidCredentialReceiving, OutboundCommunication, SendResponseError, WalletKeystore } from "./interfaces"; +import { HandleOutboundRequestError, OutboundCommunication, SendResponseError, WalletKeystore } from "./interfaces"; import { TYPES } from "./types"; import { OutboundRequest } from "./types/OutboundRequest"; import { getAllVerifiableCredentials } from "../entities/VerifiableCredential.entity"; @@ -87,7 +87,6 @@ export class OpenidForPresentationService implements OutboundCommunication { constructor( @inject(TYPES.WalletKeystoreManagerService) private walletKeystoreManagerService: WalletKeystore, @inject(TYPES.VerifierRegistryService) private verifierRegistryService: VerifierRegistryService, - @inject(TYPES.OpenidForCredentialIssuanceService) private OpenidCredentialReceivingService: OpenidCredentialReceiving ) { } async initiateVerificationFlow(userId: UserId, verifierId: number, scopeName: string): Promise<{ redirect_to?: string }> { From b49897f60dc50ce76675f408716f4ad8ba43e450 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:02:27 +0200 Subject: [PATCH 48/51] Delete unused interface method createIdToken --- src/services/ClientKeystoreService.ts | 23 ------------------ src/services/DatabaseKeystoreService.ts | 25 -------------------- src/services/WalletKeystoreManagerService.ts | 12 ---------- src/services/interfaces.ts | 2 -- src/services/shared.types.ts | 3 --- 5 files changed, 65 deletions(-) diff --git a/src/services/ClientKeystoreService.ts b/src/services/ClientKeystoreService.ts index 4c29b2f..bb859f0 100644 --- a/src/services/ClientKeystoreService.ts +++ b/src/services/ClientKeystoreService.ts @@ -23,29 +23,6 @@ export class ClientKeystoreService implements WalletKeystore { ) { } - async createIdToken(userId: UserId, 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(userId, msg as ServerSocketMessage) - - const result = await this.socketManagerService.expect(userId, 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(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters: AdditionalKeystoreParameters): Promise> { let message_id_sent = randomUUID(); const msg = { diff --git a/src/services/DatabaseKeystoreService.ts b/src/services/DatabaseKeystoreService.ts index 499254d..de40efd 100644 --- a/src/services/DatabaseKeystoreService.ts +++ b/src/services/DatabaseKeystoreService.ts @@ -33,31 +33,6 @@ export class DatabaseKeystoreService implements WalletKeystore { } - async createIdToken(userId: UserId, nonce: string, audience: 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); - } - - 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(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; diff --git a/src/services/WalletKeystoreManagerService.ts b/src/services/WalletKeystoreManagerService.ts index 2c3780d..97d26e6 100644 --- a/src/services/WalletKeystoreManagerService.ts +++ b/src/services/WalletKeystoreManagerService.ts @@ -48,18 +48,6 @@ export class WalletKeystoreManagerService implements WalletKeystoreManager { } } - async createIdToken(userId: UserId, nonce: string, audience: 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.createIdToken(userId, nonce, audience, additionalParameters); - else - return await this.databaseKeystoreService.createIdToken(userId, nonce, audience, additionalParameters); - } - async signJwtPresentation(userId: UserId, nonce: string, audience: string, verifiableCredentials: any[], additionalParameters?: AdditionalKeystoreParameters): Promise> { const userRes = await getUser(userId) if (userRes.err) { diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 54d73cd..2a0d7c7 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -45,14 +45,12 @@ export type RegistrationParams = { export interface WalletKeystoreManager { initializeWallet(registrationParams: RegistrationParams): Promise>; - createIdToken(userId: UserId, nonce: string, audience: 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(userId: UserId, nonce: string, audience: 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>; } 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 } ); From 0a7cd1ba529d57a22f692a67f1bf90470f291d52 Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:07:52 +0200 Subject: [PATCH 49/51] Delete unused interface method OpenidCredentialReceiving.getIssuerState --- src/services/OpenidForCredentialIssuanceService.ts | 12 ------------ src/services/interfaces.ts | 2 -- 2 files changed, 14 deletions(-) diff --git a/src/services/OpenidForCredentialIssuanceService.ts b/src/services/OpenidForCredentialIssuanceService.ts index 889efbd..0f8d3ee 100644 --- a/src/services/OpenidForCredentialIssuanceService.ts +++ b/src/services/OpenidForCredentialIssuanceService.ts @@ -46,18 +46,6 @@ export class OpenidForCredentialIssuanceService implements OpenidCredentialRecei ) { } - async getIssuerState(userId: UserId): Promise<{ issuer_state?: string, error?: Error; }> { - const state = this.states.get(userId.id); - 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 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); diff --git a/src/services/interfaces.ts b/src/services/interfaces.ts index 2a0d7c7..65e62cd 100644 --- a/src/services/interfaces.ts +++ b/src/services/interfaces.ts @@ -13,8 +13,6 @@ export interface OpenidCredentialReceiving { handleAuthorizationResponse(userId: UserId, authorizationResponseURL: string): Promise>; requestCredentialsWithPreAuthorizedGrant(userId: UserId, user_pin: string): Promise<{error?: string}>; - - getIssuerState(userId: UserId): Promise<{ issuer_state?: string, error?: Error }> } export enum IssuanceErr { From ee9acf29ebda170ce4b6c4e496e884bfa545fabe Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:08:47 +0200 Subject: [PATCH 50/51] Remove unused import --- src/routers/communicationHandler.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routers/communicationHandler.router.ts b/src/routers/communicationHandler.router.ts index cb982b0..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'; From add10442063a20e5bce288e705648b6e7ee4eb6b Mon Sep 17 00:00:00 2001 From: Emil Lundberg Date: Tue, 13 Aug 2024 15:23:18 +0200 Subject: [PATCH 51/51] Delete unused local variable --- src/services/OpenidForPresentationService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/OpenidForPresentationService.ts b/src/services/OpenidForPresentationService.ts index bbbaf37..904ba7d 100644 --- a/src/services/OpenidForPresentationService.ts +++ b/src/services/OpenidForPresentationService.ts @@ -362,7 +362,6 @@ export class OpenidForPresentationService implements OutboundCommunication { .filter((vc) => allSelectedCredentialIdentifiers.includes(vc.credentialIdentifier) ); - const filteredVCJwtList = filteredVCEntities.map((vc) => vc.credential); try { const fetchedState = this.states.get(userId.id);