From faa01a291f60d61e93e9d5577b54b6f1b08989c6 Mon Sep 17 00:00:00 2001
From: Christopher Guindon <chris.guindon@eclipse-foundation.org>
Date: Thu, 14 Oct 2021 14:02:51 -0400
Subject: [PATCH 01/11] Update README.md

---
 README.md | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/README.md b/README.md
index 82e7fbcb..5c11f32b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,12 @@
 # git-eca-rest-api
 
+---
+**NOTE**
+
+This project was migrated to [Eclipse Gitlab](https://gitlab.eclipse.org/eclipsefdn/it/api/git-eca-rest-api) on October 14, 2021.
+
+---
+
 This project uses Quarkus, the Supersonic Subatomic Java Framework.
 
 If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ .
-- 
GitLab


From 6c9bee598601ab70aae4d8b10f355d939ab8ce04 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 26 Jan 2022 16:21:10 -0500
Subject: [PATCH 02/11] Implement EF commons lib, upgrade to Quarkus 2.6, use
 autovalue for pojo

---
 .gitignore                                    |    3 +
 package-lock.json                             | 2087 +++++++++++++++++
 package.json                                  |   20 +
 pom.xml                                       |  211 +-
 .../git/eca/api/AccountsAPI.java              |   13 +-
 .../git/eca/config/CustomJacksonConfig.java   |   33 -
 .../git/eca/config/SecretConfigSource.java    |   92 -
 .../git/eca/model/Commit.java                 |  149 +-
 .../git/eca/model/CommitStatus.java           |  192 +-
 .../git/eca/model/EclipseUser.java            |  204 +-
 .../git/eca/model/GitUser.java                |   61 +-
 .../git/eca/model/Project.java                |  312 +--
 .../git/eca/model/ValidationRequest.java      |   90 +-
 .../git/eca/model/ValidationResponse.java     |  189 +-
 .../git/eca/oauth/EclipseApi.java             |   47 -
 .../git/eca/resource/ValidationResource.java  |  361 +--
 .../git/eca/service/CachingService.java       |   74 -
 .../git/eca/service/OAuthService.java         |   31 -
 .../eca/service/impl/DefaultOAuthService.java |   90 -
 .../eca/service/impl/GuavaCachingService.java |  137 --
 src/main/js/openapi2schema.js                 |   54 +
 ...lipse.microprofile.config.spi.ConfigSource |    1 -
 src/main/resources/application.properties     |   12 +-
 .../git/eca/api/MockAccountsAPI.java          |  121 +-
 .../git/eca/api/MockProjectsAPI.java          |  107 +-
 .../git/eca/helper/CommitHelperTest.java      |  177 +-
 .../eca/resource/ValidationResourceTest.java  | 1902 ++++++---------
 .../eca/service/impl/MockOAuthService.java    |   24 -
 src/test/resources/application.properties     |   14 +-
 29 files changed, 3903 insertions(+), 2905 deletions(-)
 create mode 100644 package-lock.json
 create mode 100644 package.json
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/config/CustomJacksonConfig.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/config/SecretConfigSource.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/CachingService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/GuavaCachingService.java
 create mode 100644 src/main/js/openapi2schema.js
 delete mode 100644 src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
 delete mode 100644 src/test/java/org/eclipsefoundation/git/eca/service/impl/MockOAuthService.java

diff --git a/.gitignore b/.gitignore
index 3b04c571..6604264c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,9 @@ nb-configuration.xml
 *.orig
 *.rej
 
+# npm
+node_modules
+
 # Maven
 target/
 pom.xml.tag
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..e116e830
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2087 @@
+{
+    "name": "eclipsefdn-git-eca-rest-api-support",
+    "version": "1.0.0",
+    "lockfileVersion": 2,
+    "requires": true,
+    "packages": {
+        "": {
+            "name": "eclipsefdn-git-eca-rest-api-support",
+            "version": "1.0.0",
+            "devDependencies": {
+                "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1",
+                "@redocly/openapi-cli": "^1.0.0-beta.54",
+                "@stoplight/json-ref-resolver": "^3.1.2",
+                "decamelize": "^5.0.0",
+                "js-yaml": "^4.1.0",
+                "yargs": "^17.0.1"
+            }
+        },
+        "node_modules/@openapi-contrib/openapi-schema-to-json-schema": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz",
+            "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==",
+            "dev": true,
+            "dependencies": {
+                "fast-deep-equal": "^3.1.3"
+            }
+        },
+        "node_modules/@redocly/ajv": {
+            "version": "8.6.4",
+            "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.4.tgz",
+            "integrity": "sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==",
+            "dev": true,
+            "dependencies": {
+                "fast-deep-equal": "^3.1.1",
+                "json-schema-traverse": "^1.0.0",
+                "require-from-string": "^2.0.2",
+                "uri-js": "^4.2.2"
+            },
+            "funding": {
+                "type": "github",
+                "url": "https://github.com/sponsors/epoberezkin"
+            }
+        },
+        "node_modules/@redocly/openapi-cli": {
+            "version": "1.0.0-beta.80",
+            "resolved": "https://registry.npmjs.org/@redocly/openapi-cli/-/openapi-cli-1.0.0-beta.80.tgz",
+            "integrity": "sha512-PZLustQdB0ZsKjM5vgGxEg6eFf8okaky/LmXiicnNeJhXmlv7CmvytSvm6zixCwDts4DpuFUNO1CcDf7+iJyDA==",
+            "dev": true,
+            "dependencies": {
+                "@redocly/openapi-core": "1.0.0-beta.80",
+                "@types/node": "^14.11.8",
+                "assert-node-version": "^1.0.3",
+                "chokidar": "^3.5.1",
+                "colorette": "^1.2.0",
+                "glob": "^7.1.6",
+                "glob-promise": "^3.4.0",
+                "handlebars": "^4.7.6",
+                "portfinder": "^1.0.26",
+                "simple-websocket": "^9.0.0",
+                "yargs": "17.0.1"
+            },
+            "bin": {
+                "openapi": "bin/cli.js"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/@redocly/openapi-cli/node_modules/yargs": {
+            "version": "17.0.1",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz",
+            "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==",
+            "dev": true,
+            "dependencies": {
+                "cliui": "^7.0.2",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
+                "require-directory": "^2.1.1",
+                "string-width": "^4.2.0",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^20.2.2"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/@redocly/openapi-cli/node_modules/yargs-parser": {
+            "version": "20.2.9",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+            "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/@redocly/openapi-core": {
+            "version": "1.0.0-beta.80",
+            "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz",
+            "integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==",
+            "dev": true,
+            "dependencies": {
+                "@redocly/ajv": "^8.6.4",
+                "@types/node": "^14.11.8",
+                "colorette": "^1.2.0",
+                "js-levenshtein": "^1.1.6",
+                "js-yaml": "^4.1.0",
+                "lodash.isequal": "^4.5.0",
+                "minimatch": "^3.0.4",
+                "node-fetch": "^2.6.1",
+                "pluralize": "^8.0.0",
+                "yaml-ast-parser": "0.0.43"
+            },
+            "engines": {
+                "node": ">=12.0.0"
+            }
+        },
+        "node_modules/@stoplight/json": {
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.17.2.tgz",
+            "integrity": "sha512-NwIVzanXRUy291J5BMkncCZRMG1Lx+aq+VidGQgfkJjgo8vh1Y/PSAz7fSU8gVGSZBCcqmOkMI7R4zw7DlfTwA==",
+            "dev": true,
+            "dependencies": {
+                "@stoplight/ordered-object-literal": "^1.0.2",
+                "@stoplight/types": "^12.3.0",
+                "jsonc-parser": "~2.2.1",
+                "lodash": "^4.17.21",
+                "safe-stable-stringify": "^1.1"
+            },
+            "engines": {
+                "node": ">=8.3.0"
+            }
+        },
+        "node_modules/@stoplight/json-ref-resolver": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.3.tgz",
+            "integrity": "sha512-SgoKXwVnlpIZUyAFX4W79eeuTWvXmNlMfICZixL16GZXnkjcW+uZnfmAU0ZIjcnaTgaI4mjfxn8LAP2KR6Cr0A==",
+            "dev": true,
+            "dependencies": {
+                "@stoplight/json": "^3.17.0",
+                "@stoplight/path": "^1.3.2",
+                "@stoplight/types": "^12.3.0",
+                "@types/urijs": "^1.19.16",
+                "dependency-graph": "~0.11.0",
+                "fast-memoize": "^2.5.2",
+                "immer": "^9.0.6",
+                "lodash.get": "^4.4.2",
+                "lodash.set": "^4.3.2",
+                "tslib": "^2.3.1",
+                "urijs": "^1.19.6"
+            },
+            "engines": {
+                "node": ">=8.3.0"
+            }
+        },
+        "node_modules/@stoplight/ordered-object-literal": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz",
+            "integrity": "sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@stoplight/path": {
+            "version": "1.3.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz",
+            "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@stoplight/types": {
+            "version": "12.5.0",
+            "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-12.5.0.tgz",
+            "integrity": "sha512-dwqYcDrGmEyUv5TWrDam5TGOxU72ufyQ7hnOIIDdmW5ezOwZaBFoR5XQ9AsH49w7wgvOqB2Bmo799pJPWnpCbg==",
+            "dev": true,
+            "dependencies": {
+                "@types/json-schema": "^7.0.4",
+                "utility-types": "^3.10.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/@types/glob": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+            "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+            "dev": true,
+            "dependencies": {
+                "@types/minimatch": "*",
+                "@types/node": "*"
+            }
+        },
+        "node_modules/@types/json-schema": {
+            "version": "7.0.9",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
+            "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
+            "dev": true
+        },
+        "node_modules/@types/minimatch": {
+            "version": "3.0.5",
+            "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+            "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+            "dev": true
+        },
+        "node_modules/@types/node": {
+            "version": "14.18.9",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
+            "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==",
+            "dev": true
+        },
+        "node_modules/@types/urijs": {
+            "version": "1.19.18",
+            "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.18.tgz",
+            "integrity": "sha512-tjftsOLuIWFLJxcpgFeehNnMhpMIv0ELJl0/i31jiV3au1GQpnd3/pTTDQg2zO5cSGJxtrDzMgebOH7+cqh3Vg==",
+            "dev": true
+        },
+        "node_modules/ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "dependencies": {
+                "color-convert": "^2.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+            }
+        },
+        "node_modules/anymatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+            "dev": true,
+            "dependencies": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            },
+            "engines": {
+                "node": ">= 8"
+            }
+        },
+        "node_modules/argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true
+        },
+        "node_modules/assert-node-version": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/assert-node-version/-/assert-node-version-1.0.3.tgz",
+            "integrity": "sha1-yupdG2pY285ZZhII3x4bnkxYD5E=",
+            "dev": true,
+            "dependencies": {
+                "expected-node-version": "^1.0.0",
+                "semver": "^5.0.3"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/async": {
+            "version": "2.6.3",
+            "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+            "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
+            "dev": true,
+            "dependencies": {
+                "lodash": "^4.17.14"
+            }
+        },
+        "node_modules/balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
+        "node_modules/binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "dependencies": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "node_modules/braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "dependencies": {
+                "fill-range": "^7.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://paulmillr.com/funding/"
+                }
+            ],
+            "dependencies": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            },
+            "engines": {
+                "node": ">= 8.10.0"
+            },
+            "optionalDependencies": {
+                "fsevents": "~2.3.2"
+            }
+        },
+        "node_modules/cliui": {
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+            "dev": true,
+            "dependencies": {
+                "string-width": "^4.2.0",
+                "strip-ansi": "^6.0.0",
+                "wrap-ansi": "^7.0.0"
+            }
+        },
+        "node_modules/color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "dependencies": {
+                "color-name": "~1.1.4"
+            },
+            "engines": {
+                "node": ">=7.0.0"
+            }
+        },
+        "node_modules/color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "node_modules/colorette": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+            "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
+            "dev": true
+        },
+        "node_modules/concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+            "dev": true
+        },
+        "node_modules/debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dev": true,
+            "dependencies": {
+                "ms": "^2.1.1"
+            }
+        },
+        "node_modules/decamelize": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
+            "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/sindresorhus"
+            }
+        },
+        "node_modules/dependency-graph": {
+            "version": "0.11.0",
+            "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
+            "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.6.0"
+            }
+        },
+        "node_modules/emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+            "dev": true
+        },
+        "node_modules/escalade": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/expected-node-version": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/expected-node-version/-/expected-node-version-1.0.2.tgz",
+            "integrity": "sha1-uNIlub9nap6H4G29YVtS/J0eOGs=",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "dev": true
+        },
+        "node_modules/fast-memoize": {
+            "version": "2.5.2",
+            "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
+            "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==",
+            "dev": true
+        },
+        "node_modules/fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "dependencies": {
+                "to-regex-range": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+            "dev": true
+        },
+        "node_modules/get-caller-file": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+            "dev": true,
+            "engines": {
+                "node": "6.* || 8.* || >= 10.*"
+            }
+        },
+        "node_modules/glob": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+            "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+            "dev": true,
+            "dependencies": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.0.4",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            },
+            "engines": {
+                "node": "*"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/isaacs"
+            }
+        },
+        "node_modules/glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "dependencies": {
+                "is-glob": "^4.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/glob-promise": {
+            "version": "3.4.0",
+            "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz",
+            "integrity": "sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==",
+            "dev": true,
+            "dependencies": {
+                "@types/glob": "*"
+            },
+            "engines": {
+                "node": ">=4"
+            },
+            "peerDependencies": {
+                "glob": "*"
+            }
+        },
+        "node_modules/handlebars": {
+            "version": "4.7.7",
+            "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+            "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
+            "dev": true,
+            "dependencies": {
+                "minimist": "^1.2.5",
+                "neo-async": "^2.6.0",
+                "source-map": "^0.6.1",
+                "wordwrap": "^1.0.0"
+            },
+            "bin": {
+                "handlebars": "bin/handlebars"
+            },
+            "engines": {
+                "node": ">=0.4.7"
+            },
+            "optionalDependencies": {
+                "uglify-js": "^3.1.4"
+            }
+        },
+        "node_modules/immer": {
+            "version": "9.0.12",
+            "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
+            "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
+            "dev": true,
+            "funding": {
+                "type": "opencollective",
+                "url": "https://opencollective.com/immer"
+            }
+        },
+        "node_modules/inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+            "dev": true,
+            "dependencies": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "node_modules/inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+            "dev": true
+        },
+        "node_modules/is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dev": true,
+            "dependencies": {
+                "binary-extensions": "^2.0.0"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "dev": true,
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "dependencies": {
+                "is-extglob": "^2.1.1"
+            },
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.12.0"
+            }
+        },
+        "node_modules/js-levenshtein": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+            "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "dependencies": {
+                "argparse": "^2.0.1"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/json-schema-traverse": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+            "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+            "dev": true
+        },
+        "node_modules/jsonc-parser": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz",
+            "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==",
+            "dev": true
+        },
+        "node_modules/lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+            "dev": true
+        },
+        "node_modules/lodash.get": {
+            "version": "4.4.2",
+            "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+            "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+            "dev": true
+        },
+        "node_modules/lodash.isequal": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+            "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+            "dev": true
+        },
+        "node_modules/lodash.set": {
+            "version": "4.3.2",
+            "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+            "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
+            "dev": true
+        },
+        "node_modules/minimatch": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+            "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+            "dev": true,
+            "dependencies": {
+                "brace-expansion": "^1.1.7"
+            },
+            "engines": {
+                "node": "*"
+            }
+        },
+        "node_modules/minimist": {
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+            "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+            "dev": true
+        },
+        "node_modules/mkdirp": {
+            "version": "0.5.5",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+            "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+            "dev": true,
+            "dependencies": {
+                "minimist": "^1.2.5"
+            },
+            "bin": {
+                "mkdirp": "bin/cmd.js"
+            }
+        },
+        "node_modules/ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "dev": true
+        },
+        "node_modules/neo-async": {
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+            "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+            "dev": true
+        },
+        "node_modules/node-fetch": {
+            "version": "2.6.7",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+            "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+            "dev": true,
+            "dependencies": {
+                "whatwg-url": "^5.0.0"
+            },
+            "engines": {
+                "node": "4.x || >=6.0.0"
+            },
+            "peerDependencies": {
+                "encoding": "^0.1.0"
+            },
+            "peerDependenciesMeta": {
+                "encoding": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+            "dev": true,
+            "dependencies": {
+                "wrappy": "1"
+            }
+        },
+        "node_modules/path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.6"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/jonschlinkert"
+            }
+        },
+        "node_modules/pluralize": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+            "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+            "dev": true,
+            "engines": {
+                "node": ">=4"
+            }
+        },
+        "node_modules/portfinder": {
+            "version": "1.0.28",
+            "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
+            "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
+            "dev": true,
+            "dependencies": {
+                "async": "^2.6.2",
+                "debug": "^3.1.1",
+                "mkdirp": "^0.5.5"
+            },
+            "engines": {
+                "node": ">= 0.12.0"
+            }
+        },
+        "node_modules/punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+            "dev": true,
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/randombytes": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+            "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+            "dev": true,
+            "dependencies": {
+                "safe-buffer": "^5.1.0"
+            }
+        },
+        "node_modules/readable-stream": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+            "dev": true,
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
+        "node_modules/readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "dependencies": {
+                "picomatch": "^2.2.1"
+            },
+            "engines": {
+                "node": ">=8.10.0"
+            }
+        },
+        "node_modules/require-directory": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+            "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/require-from-string": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+            "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ]
+        },
+        "node_modules/safe-stable-stringify": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
+            "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==",
+            "dev": true
+        },
+        "node_modules/semver": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+            "dev": true,
+            "bin": {
+                "semver": "bin/semver"
+            }
+        },
+        "node_modules/simple-websocket": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
+            "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==",
+            "dev": true,
+            "funding": [
+                {
+                    "type": "github",
+                    "url": "https://github.com/sponsors/feross"
+                },
+                {
+                    "type": "patreon",
+                    "url": "https://www.patreon.com/feross"
+                },
+                {
+                    "type": "consulting",
+                    "url": "https://feross.org/support"
+                }
+            ],
+            "dependencies": {
+                "debug": "^4.3.1",
+                "queue-microtask": "^1.2.2",
+                "randombytes": "^2.1.0",
+                "readable-stream": "^3.6.0",
+                "ws": "^7.4.2"
+            }
+        },
+        "node_modules/simple-websocket/node_modules/debug": {
+            "version": "4.3.3",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+            "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+            "dev": true,
+            "dependencies": {
+                "ms": "2.1.2"
+            },
+            "engines": {
+                "node": ">=6.0"
+            },
+            "peerDependenciesMeta": {
+                "supports-color": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/simple-websocket/node_modules/ms": {
+            "version": "2.1.2",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+            "dev": true
+        },
+        "node_modules/source-map": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "dev": true,
+            "engines": {
+                "node": ">=0.10.0"
+            }
+        },
+        "node_modules/string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "dev": true,
+            "dependencies": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "node_modules/string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "dev": true,
+            "dependencies": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "dependencies": {
+                "ansi-regex": "^5.0.1"
+            },
+            "engines": {
+                "node": ">=8"
+            }
+        },
+        "node_modules/to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "dependencies": {
+                "is-number": "^7.0.0"
+            },
+            "engines": {
+                "node": ">=8.0"
+            }
+        },
+        "node_modules/tr46": {
+            "version": "0.0.3",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+            "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+            "dev": true
+        },
+        "node_modules/tslib": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+            "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+            "dev": true
+        },
+        "node_modules/uglify-js": {
+            "version": "3.14.5",
+            "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz",
+            "integrity": "sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ==",
+            "dev": true,
+            "optional": true,
+            "bin": {
+                "uglifyjs": "bin/uglifyjs"
+            },
+            "engines": {
+                "node": ">=0.8.0"
+            }
+        },
+        "node_modules/uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dev": true,
+            "dependencies": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "node_modules/urijs": {
+            "version": "1.19.7",
+            "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz",
+            "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==",
+            "dev": true
+        },
+        "node_modules/util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+            "dev": true
+        },
+        "node_modules/utility-types": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 4"
+            }
+        },
+        "node_modules/webidl-conversions": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+            "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+            "dev": true
+        },
+        "node_modules/whatwg-url": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+            "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+            "dev": true,
+            "dependencies": {
+                "tr46": "~0.0.3",
+                "webidl-conversions": "^3.0.0"
+            }
+        },
+        "node_modules/wordwrap": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+            "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+            "dev": true
+        },
+        "node_modules/wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dev": true,
+            "dependencies": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            },
+            "engines": {
+                "node": ">=10"
+            },
+            "funding": {
+                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+            }
+        },
+        "node_modules/wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+            "dev": true
+        },
+        "node_modules/ws": {
+            "version": "7.5.6",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
+            "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
+            "dev": true,
+            "engines": {
+                "node": ">=8.3.0"
+            },
+            "peerDependencies": {
+                "bufferutil": "^4.0.1",
+                "utf-8-validate": "^5.0.2"
+            },
+            "peerDependenciesMeta": {
+                "bufferutil": {
+                    "optional": true
+                },
+                "utf-8-validate": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/y18n": {
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+            "dev": true,
+            "engines": {
+                "node": ">=10"
+            }
+        },
+        "node_modules/yaml-ast-parser": {
+            "version": "0.0.43",
+            "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
+            "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
+            "dev": true
+        },
+        "node_modules/yargs": {
+            "version": "17.3.1",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
+            "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+            "dev": true,
+            "dependencies": {
+                "cliui": "^7.0.2",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
+                "require-directory": "^2.1.1",
+                "string-width": "^4.2.3",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^21.0.0"
+            },
+            "engines": {
+                "node": ">=12"
+            }
+        },
+        "node_modules/yargs-parser": {
+            "version": "21.0.0",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
+            "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
+            "dev": true,
+            "engines": {
+                "node": ">=12"
+            }
+        }
+    },
+    "dependencies": {
+        "@openapi-contrib/openapi-schema-to-json-schema": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz",
+            "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==",
+            "dev": true,
+            "requires": {
+                "fast-deep-equal": "^3.1.3"
+            }
+        },
+        "@redocly/ajv": {
+            "version": "8.6.4",
+            "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.4.tgz",
+            "integrity": "sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==",
+            "dev": true,
+            "requires": {
+                "fast-deep-equal": "^3.1.1",
+                "json-schema-traverse": "^1.0.0",
+                "require-from-string": "^2.0.2",
+                "uri-js": "^4.2.2"
+            }
+        },
+        "@redocly/openapi-cli": {
+            "version": "1.0.0-beta.80",
+            "resolved": "https://registry.npmjs.org/@redocly/openapi-cli/-/openapi-cli-1.0.0-beta.80.tgz",
+            "integrity": "sha512-PZLustQdB0ZsKjM5vgGxEg6eFf8okaky/LmXiicnNeJhXmlv7CmvytSvm6zixCwDts4DpuFUNO1CcDf7+iJyDA==",
+            "dev": true,
+            "requires": {
+                "@redocly/openapi-core": "1.0.0-beta.80",
+                "@types/node": "^14.11.8",
+                "assert-node-version": "^1.0.3",
+                "chokidar": "^3.5.1",
+                "colorette": "^1.2.0",
+                "glob": "^7.1.6",
+                "glob-promise": "^3.4.0",
+                "handlebars": "^4.7.6",
+                "portfinder": "^1.0.26",
+                "simple-websocket": "^9.0.0",
+                "yargs": "17.0.1"
+            },
+            "dependencies": {
+                "yargs": {
+                    "version": "17.0.1",
+                    "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz",
+                    "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==",
+                    "dev": true,
+                    "requires": {
+                        "cliui": "^7.0.2",
+                        "escalade": "^3.1.1",
+                        "get-caller-file": "^2.0.5",
+                        "require-directory": "^2.1.1",
+                        "string-width": "^4.2.0",
+                        "y18n": "^5.0.5",
+                        "yargs-parser": "^20.2.2"
+                    }
+                },
+                "yargs-parser": {
+                    "version": "20.2.9",
+                    "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+                    "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+                    "dev": true
+                }
+            }
+        },
+        "@redocly/openapi-core": {
+            "version": "1.0.0-beta.80",
+            "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz",
+            "integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==",
+            "dev": true,
+            "requires": {
+                "@redocly/ajv": "^8.6.4",
+                "@types/node": "^14.11.8",
+                "colorette": "^1.2.0",
+                "js-levenshtein": "^1.1.6",
+                "js-yaml": "^4.1.0",
+                "lodash.isequal": "^4.5.0",
+                "minimatch": "^3.0.4",
+                "node-fetch": "^2.6.1",
+                "pluralize": "^8.0.0",
+                "yaml-ast-parser": "0.0.43"
+            }
+        },
+        "@stoplight/json": {
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.17.2.tgz",
+            "integrity": "sha512-NwIVzanXRUy291J5BMkncCZRMG1Lx+aq+VidGQgfkJjgo8vh1Y/PSAz7fSU8gVGSZBCcqmOkMI7R4zw7DlfTwA==",
+            "dev": true,
+            "requires": {
+                "@stoplight/ordered-object-literal": "^1.0.2",
+                "@stoplight/types": "^12.3.0",
+                "jsonc-parser": "~2.2.1",
+                "lodash": "^4.17.21",
+                "safe-stable-stringify": "^1.1"
+            }
+        },
+        "@stoplight/json-ref-resolver": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.3.tgz",
+            "integrity": "sha512-SgoKXwVnlpIZUyAFX4W79eeuTWvXmNlMfICZixL16GZXnkjcW+uZnfmAU0ZIjcnaTgaI4mjfxn8LAP2KR6Cr0A==",
+            "dev": true,
+            "requires": {
+                "@stoplight/json": "^3.17.0",
+                "@stoplight/path": "^1.3.2",
+                "@stoplight/types": "^12.3.0",
+                "@types/urijs": "^1.19.16",
+                "dependency-graph": "~0.11.0",
+                "fast-memoize": "^2.5.2",
+                "immer": "^9.0.6",
+                "lodash.get": "^4.4.2",
+                "lodash.set": "^4.3.2",
+                "tslib": "^2.3.1",
+                "urijs": "^1.19.6"
+            }
+        },
+        "@stoplight/ordered-object-literal": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz",
+            "integrity": "sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==",
+            "dev": true
+        },
+        "@stoplight/path": {
+            "version": "1.3.2",
+            "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz",
+            "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==",
+            "dev": true
+        },
+        "@stoplight/types": {
+            "version": "12.5.0",
+            "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-12.5.0.tgz",
+            "integrity": "sha512-dwqYcDrGmEyUv5TWrDam5TGOxU72ufyQ7hnOIIDdmW5ezOwZaBFoR5XQ9AsH49w7wgvOqB2Bmo799pJPWnpCbg==",
+            "dev": true,
+            "requires": {
+                "@types/json-schema": "^7.0.4",
+                "utility-types": "^3.10.0"
+            }
+        },
+        "@types/glob": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
+            "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
+            "dev": true,
+            "requires": {
+                "@types/minimatch": "*",
+                "@types/node": "*"
+            }
+        },
+        "@types/json-schema": {
+            "version": "7.0.9",
+            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
+            "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
+            "dev": true
+        },
+        "@types/minimatch": {
+            "version": "3.0.5",
+            "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+            "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+            "dev": true
+        },
+        "@types/node": {
+            "version": "14.18.9",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
+            "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==",
+            "dev": true
+        },
+        "@types/urijs": {
+            "version": "1.19.18",
+            "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.18.tgz",
+            "integrity": "sha512-tjftsOLuIWFLJxcpgFeehNnMhpMIv0ELJl0/i31jiV3au1GQpnd3/pTTDQg2zO5cSGJxtrDzMgebOH7+cqh3Vg==",
+            "dev": true
+        },
+        "ansi-regex": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+            "dev": true
+        },
+        "ansi-styles": {
+            "version": "4.3.0",
+            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+            "dev": true,
+            "requires": {
+                "color-convert": "^2.0.1"
+            }
+        },
+        "anymatch": {
+            "version": "3.1.2",
+            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+            "dev": true,
+            "requires": {
+                "normalize-path": "^3.0.0",
+                "picomatch": "^2.0.4"
+            }
+        },
+        "argparse": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+            "dev": true
+        },
+        "assert-node-version": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/assert-node-version/-/assert-node-version-1.0.3.tgz",
+            "integrity": "sha1-yupdG2pY285ZZhII3x4bnkxYD5E=",
+            "dev": true,
+            "requires": {
+                "expected-node-version": "^1.0.0",
+                "semver": "^5.0.3"
+            }
+        },
+        "async": {
+            "version": "2.6.3",
+            "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
+            "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
+            "dev": true,
+            "requires": {
+                "lodash": "^4.17.14"
+            }
+        },
+        "balanced-match": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+            "dev": true
+        },
+        "binary-extensions": {
+            "version": "2.2.0",
+            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+            "dev": true
+        },
+        "brace-expansion": {
+            "version": "1.1.11",
+            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+            "dev": true,
+            "requires": {
+                "balanced-match": "^1.0.0",
+                "concat-map": "0.0.1"
+            }
+        },
+        "braces": {
+            "version": "3.0.2",
+            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+            "dev": true,
+            "requires": {
+                "fill-range": "^7.0.1"
+            }
+        },
+        "chokidar": {
+            "version": "3.5.3",
+            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
+            "dev": true,
+            "requires": {
+                "anymatch": "~3.1.2",
+                "braces": "~3.0.2",
+                "fsevents": "~2.3.2",
+                "glob-parent": "~5.1.2",
+                "is-binary-path": "~2.1.0",
+                "is-glob": "~4.0.1",
+                "normalize-path": "~3.0.0",
+                "readdirp": "~3.6.0"
+            }
+        },
+        "cliui": {
+            "version": "7.0.4",
+            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+            "dev": true,
+            "requires": {
+                "string-width": "^4.2.0",
+                "strip-ansi": "^6.0.0",
+                "wrap-ansi": "^7.0.0"
+            }
+        },
+        "color-convert": {
+            "version": "2.0.1",
+            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+            "dev": true,
+            "requires": {
+                "color-name": "~1.1.4"
+            }
+        },
+        "color-name": {
+            "version": "1.1.4",
+            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+            "dev": true
+        },
+        "colorette": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
+            "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
+            "dev": true
+        },
+        "concat-map": {
+            "version": "0.0.1",
+            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+            "dev": true
+        },
+        "debug": {
+            "version": "3.2.7",
+            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+            "dev": true,
+            "requires": {
+                "ms": "^2.1.1"
+            }
+        },
+        "decamelize": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
+            "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
+            "dev": true
+        },
+        "dependency-graph": {
+            "version": "0.11.0",
+            "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
+            "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
+            "dev": true
+        },
+        "emoji-regex": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+            "dev": true
+        },
+        "escalade": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+            "dev": true
+        },
+        "expected-node-version": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/expected-node-version/-/expected-node-version-1.0.2.tgz",
+            "integrity": "sha1-uNIlub9nap6H4G29YVtS/J0eOGs=",
+            "dev": true
+        },
+        "fast-deep-equal": {
+            "version": "3.1.3",
+            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+            "dev": true
+        },
+        "fast-memoize": {
+            "version": "2.5.2",
+            "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
+            "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==",
+            "dev": true
+        },
+        "fill-range": {
+            "version": "7.0.1",
+            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+            "dev": true,
+            "requires": {
+                "to-regex-range": "^5.0.1"
+            }
+        },
+        "fs.realpath": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+            "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+            "dev": true
+        },
+        "get-caller-file": {
+            "version": "2.0.5",
+            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+            "dev": true
+        },
+        "glob": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
+            "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
+            "dev": true,
+            "requires": {
+                "fs.realpath": "^1.0.0",
+                "inflight": "^1.0.4",
+                "inherits": "2",
+                "minimatch": "^3.0.4",
+                "once": "^1.3.0",
+                "path-is-absolute": "^1.0.0"
+            }
+        },
+        "glob-parent": {
+            "version": "5.1.2",
+            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+            "dev": true,
+            "requires": {
+                "is-glob": "^4.0.1"
+            }
+        },
+        "glob-promise": {
+            "version": "3.4.0",
+            "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz",
+            "integrity": "sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==",
+            "dev": true,
+            "requires": {
+                "@types/glob": "*"
+            }
+        },
+        "handlebars": {
+            "version": "4.7.7",
+            "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
+            "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
+            "dev": true,
+            "requires": {
+                "minimist": "^1.2.5",
+                "neo-async": "^2.6.0",
+                "source-map": "^0.6.1",
+                "uglify-js": "^3.1.4",
+                "wordwrap": "^1.0.0"
+            }
+        },
+        "immer": {
+            "version": "9.0.12",
+            "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
+            "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
+            "dev": true
+        },
+        "inflight": {
+            "version": "1.0.6",
+            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+            "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+            "dev": true,
+            "requires": {
+                "once": "^1.3.0",
+                "wrappy": "1"
+            }
+        },
+        "inherits": {
+            "version": "2.0.4",
+            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+            "dev": true
+        },
+        "is-binary-path": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+            "dev": true,
+            "requires": {
+                "binary-extensions": "^2.0.0"
+            }
+        },
+        "is-extglob": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+            "dev": true
+        },
+        "is-fullwidth-code-point": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+            "dev": true
+        },
+        "is-glob": {
+            "version": "4.0.3",
+            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+            "dev": true,
+            "requires": {
+                "is-extglob": "^2.1.1"
+            }
+        },
+        "is-number": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+            "dev": true
+        },
+        "js-levenshtein": {
+            "version": "1.1.6",
+            "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
+            "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
+            "dev": true
+        },
+        "js-yaml": {
+            "version": "4.1.0",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+            "dev": true,
+            "requires": {
+                "argparse": "^2.0.1"
+            }
+        },
+        "json-schema-traverse": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+            "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+            "dev": true
+        },
+        "jsonc-parser": {
+            "version": "2.2.1",
+            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz",
+            "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==",
+            "dev": true
+        },
+        "lodash": {
+            "version": "4.17.21",
+            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+            "dev": true
+        },
+        "lodash.get": {
+            "version": "4.4.2",
+            "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+            "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
+            "dev": true
+        },
+        "lodash.isequal": {
+            "version": "4.5.0",
+            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+            "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+            "dev": true
+        },
+        "lodash.set": {
+            "version": "4.3.2",
+            "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
+            "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
+            "dev": true
+        },
+        "minimatch": {
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+            "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+            "dev": true,
+            "requires": {
+                "brace-expansion": "^1.1.7"
+            }
+        },
+        "minimist": {
+            "version": "1.2.5",
+            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+            "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+            "dev": true
+        },
+        "mkdirp": {
+            "version": "0.5.5",
+            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+            "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+            "dev": true,
+            "requires": {
+                "minimist": "^1.2.5"
+            }
+        },
+        "ms": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+            "dev": true
+        },
+        "neo-async": {
+            "version": "2.6.2",
+            "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+            "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+            "dev": true
+        },
+        "node-fetch": {
+            "version": "2.6.7",
+            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
+            "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
+            "dev": true,
+            "requires": {
+                "whatwg-url": "^5.0.0"
+            }
+        },
+        "normalize-path": {
+            "version": "3.0.0",
+            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+            "dev": true
+        },
+        "once": {
+            "version": "1.4.0",
+            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+            "dev": true,
+            "requires": {
+                "wrappy": "1"
+            }
+        },
+        "path-is-absolute": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
+            "dev": true
+        },
+        "picomatch": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+            "dev": true
+        },
+        "pluralize": {
+            "version": "8.0.0",
+            "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
+            "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
+            "dev": true
+        },
+        "portfinder": {
+            "version": "1.0.28",
+            "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
+            "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
+            "dev": true,
+            "requires": {
+                "async": "^2.6.2",
+                "debug": "^3.1.1",
+                "mkdirp": "^0.5.5"
+            }
+        },
+        "punycode": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+            "dev": true
+        },
+        "queue-microtask": {
+            "version": "1.2.3",
+            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+            "dev": true
+        },
+        "randombytes": {
+            "version": "2.1.0",
+            "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+            "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+            "dev": true,
+            "requires": {
+                "safe-buffer": "^5.1.0"
+            }
+        },
+        "readable-stream": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+            "dev": true,
+            "requires": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            }
+        },
+        "readdirp": {
+            "version": "3.6.0",
+            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+            "dev": true,
+            "requires": {
+                "picomatch": "^2.2.1"
+            }
+        },
+        "require-directory": {
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+            "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+            "dev": true
+        },
+        "require-from-string": {
+            "version": "2.0.2",
+            "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+            "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+            "dev": true
+        },
+        "safe-buffer": {
+            "version": "5.2.1",
+            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+            "dev": true
+        },
+        "safe-stable-stringify": {
+            "version": "1.1.1",
+            "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
+            "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==",
+            "dev": true
+        },
+        "semver": {
+            "version": "5.7.1",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+            "dev": true
+        },
+        "simple-websocket": {
+            "version": "9.1.0",
+            "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
+            "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==",
+            "dev": true,
+            "requires": {
+                "debug": "^4.3.1",
+                "queue-microtask": "^1.2.2",
+                "randombytes": "^2.1.0",
+                "readable-stream": "^3.6.0",
+                "ws": "^7.4.2"
+            },
+            "dependencies": {
+                "debug": {
+                    "version": "4.3.3",
+                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+                    "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
+                    "dev": true,
+                    "requires": {
+                        "ms": "2.1.2"
+                    }
+                },
+                "ms": {
+                    "version": "2.1.2",
+                    "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+                    "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+                    "dev": true
+                }
+            }
+        },
+        "source-map": {
+            "version": "0.6.1",
+            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+            "dev": true
+        },
+        "string_decoder": {
+            "version": "1.3.0",
+            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+            "dev": true,
+            "requires": {
+                "safe-buffer": "~5.2.0"
+            }
+        },
+        "string-width": {
+            "version": "4.2.3",
+            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+            "dev": true,
+            "requires": {
+                "emoji-regex": "^8.0.0",
+                "is-fullwidth-code-point": "^3.0.0",
+                "strip-ansi": "^6.0.1"
+            }
+        },
+        "strip-ansi": {
+            "version": "6.0.1",
+            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+            "dev": true,
+            "requires": {
+                "ansi-regex": "^5.0.1"
+            }
+        },
+        "to-regex-range": {
+            "version": "5.0.1",
+            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+            "dev": true,
+            "requires": {
+                "is-number": "^7.0.0"
+            }
+        },
+        "tr46": {
+            "version": "0.0.3",
+            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+            "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
+            "dev": true
+        },
+        "tslib": {
+            "version": "2.3.1",
+            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
+            "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
+            "dev": true
+        },
+        "uglify-js": {
+            "version": "3.14.5",
+            "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz",
+            "integrity": "sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ==",
+            "dev": true,
+            "optional": true
+        },
+        "uri-js": {
+            "version": "4.4.1",
+            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+            "dev": true,
+            "requires": {
+                "punycode": "^2.1.0"
+            }
+        },
+        "urijs": {
+            "version": "1.19.7",
+            "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz",
+            "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==",
+            "dev": true
+        },
+        "util-deprecate": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+            "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
+            "dev": true
+        },
+        "utility-types": {
+            "version": "3.10.0",
+            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
+            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
+            "dev": true
+        },
+        "webidl-conversions": {
+            "version": "3.0.1",
+            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+            "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
+            "dev": true
+        },
+        "whatwg-url": {
+            "version": "5.0.0",
+            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+            "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
+            "dev": true,
+            "requires": {
+                "tr46": "~0.0.3",
+                "webidl-conversions": "^3.0.0"
+            }
+        },
+        "wordwrap": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+            "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
+            "dev": true
+        },
+        "wrap-ansi": {
+            "version": "7.0.0",
+            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+            "dev": true,
+            "requires": {
+                "ansi-styles": "^4.0.0",
+                "string-width": "^4.1.0",
+                "strip-ansi": "^6.0.0"
+            }
+        },
+        "wrappy": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
+            "dev": true
+        },
+        "ws": {
+            "version": "7.5.6",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
+            "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
+            "dev": true,
+            "requires": {}
+        },
+        "y18n": {
+            "version": "5.0.8",
+            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+            "dev": true
+        },
+        "yaml-ast-parser": {
+            "version": "0.0.43",
+            "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
+            "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
+            "dev": true
+        },
+        "yargs": {
+            "version": "17.3.1",
+            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
+            "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
+            "dev": true,
+            "requires": {
+                "cliui": "^7.0.2",
+                "escalade": "^3.1.1",
+                "get-caller-file": "^2.0.5",
+                "require-directory": "^2.1.1",
+                "string-width": "^4.2.3",
+                "y18n": "^5.0.5",
+                "yargs-parser": "^21.0.0"
+            }
+        },
+        "yargs-parser": {
+            "version": "21.0.0",
+            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
+            "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
+            "dev": true
+        }
+    }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..558a65b5
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+    "name": "eclipsefdn-git-eca-rest-api-support",
+    "version": "1.0.0",
+    "devDependencies": {
+      "@redocly/openapi-cli": "^1.0.0-beta.54",
+      "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1",
+      "@stoplight/json-ref-resolver": "^3.1.2",
+      "decamelize": "^5.0.0",
+      "js-yaml": "^4.1.0",
+      "yargs": "^17.0.1"
+    },
+    "private": true,
+    "scripts": {
+      "start": "npm run generate-json-schema && npx @redocly/openapi-cli preview-docs spec/openapi.yaml -p 8093",
+      "test": "npm run generate-json-schema && npx @redocly/openapi-cli lint spec/openapi.yaml",
+      "generate-json-schema": "npm run clean && node src/main/js/openapi2schema.js -s spec/openapi.yaml -t src/test/resources",
+      "clean": "rm -rf src/test/resources/schemas/"
+    }
+  }
+  
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 1bf4edc1..c1cc7de9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,29 +1,35 @@
 <?xml version="1.0"?>
-<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
-	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.eclipsefoundation</groupId>
 	<artifactId>git-eca</artifactId>
 	<version>0.0.1</version>
 	<properties>
+		<eclipse-api-version>0.6-SNAPSHOT</eclipse-api-version>
 		<compiler-plugin.version>3.8.1</compiler-plugin.version>
 		<maven.compiler.parameters>true</maven.compiler.parameters>
 		<maven.compiler.source>1.8</maven.compiler.source>
 		<maven.compiler.target>1.8</maven.compiler.target>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-		<quarkus-plugin.version>1.3.1.Final</quarkus-plugin.version>
 		<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
 		<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
-		<quarkus.platform.version>1.3.0.Final</quarkus.platform.version>
+		<quarkus.platform.version>2.6.3.Final</quarkus.platform.version>
 		<surefire-plugin.version>2.22.1</surefire-plugin.version>
-		<sonar.sources>src/main</sonar.sources>
-		<sonar.tests>src/test</sonar.tests>
-		<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
-		<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
-		<sonar.jacoco.reportPaths>${project.build.directory}/jacoco-report</sonar.jacoco.reportPaths>
-		<sonar.junit.reportPath>${project.build.directory}/surefire-reports</sonar.junit.reportPath>
+		<auto-value.version>1.8.2</auto-value.version>
 	</properties>
+	<repositories>
+		<repository>
+			<id>eclipsefdn</id>
+			<url>https://repo.eclipse.org/content/repositories/eclipsefdn/</url>
+			<releases>
+				<enabled>true</enabled>
+			</releases>
+			<snapshots>
+				<enabled>true</enabled>
+			</snapshots>
+		</repository>
+	</repositories>
 	<dependencyManagement>
 		<dependencies>
 			<dependency>
@@ -36,54 +42,99 @@
 		</dependencies>
 	</dependencyManagement>
 	<dependencies>
+		<dependency>
+			<groupId>org.eclipsefoundation</groupId>
+			<artifactId>quarkus-core</artifactId>
+			<version>${eclipse-api-version}</version>
+		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-resteasy</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-junit5</artifactId>
-			<scope>test</scope>
+			<artifactId>quarkus-resteasy-jackson</artifactId>
 		</dependency>
 		<dependency>
-			<groupId>io.rest-assured</groupId>
-			<artifactId>rest-assured</artifactId>
-			<scope>test</scope>
+			<groupId>io.quarkus</groupId>
+			<artifactId>quarkus-smallrye-context-propagation</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-resteasy-jackson</artifactId>
+			<artifactId>quarkus-oidc</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-rest-client</artifactId>
+			<artifactId>quarkus-oidc-client-filter</artifactId>
 		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-smallrye-context-propagation</artifactId>
+			<artifactId>quarkus-rest-client</artifactId>
+		</dependency>
+		<!-- Annotation preprocessors - reduce all of the boiler plate -->
+		<dependency>
+			<groupId>com.google.auto.value</groupId>
+			<artifactId>auto-value</artifactId>
+			<version>${auto-value.version}</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>com.google.auto.value</groupId>
+			<artifactId>auto-value-annotations</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>com.google.code.findbugs</groupId>
+			<artifactId>jsr305</artifactId>
+		</dependency>
+
+		<!-- Third-party reqs -->
 		<dependency>
 			<groupId>com.github.scribejava</groupId>
 			<artifactId>scribejava-apis</artifactId>
 			<version>6.4.1</version>
 		</dependency>
-		
 		<!-- Caching -->
 		<dependency>
 			<groupId>com.google.guava</groupId>
 			<artifactId>guava</artifactId>
 		</dependency>
+
+		<!-- Test requirements -->
+		<dependency>
+			<groupId>io.quarkus</groupId>
+			<artifactId>quarkus-junit5</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.rest-assured</groupId>
+			<artifactId>rest-assured</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.rest-assured</groupId>
+			<artifactId>json-schema-validator</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>io.quarkus</groupId>
+			<artifactId>quarkus-junit5-mockito</artifactId>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
+
 	<build>
 		<plugins>
 			<plugin>
-				<groupId>io.quarkus</groupId>
+				<groupId>${quarkus.platform.group-id}</groupId>
 				<artifactId>quarkus-maven-plugin</artifactId>
-				<version>${quarkus-plugin.version}</version>
+				<version>${quarkus.platform.version}</version>
+				<extensions>true</extensions>
 				<executions>
 					<execution>
 						<goals>
 							<goal>build</goal>
+							<goal>generate-code</goal>
+							<goal>generate-code-tests</goal>
 						</goals>
 					</execution>
 				</executions>
@@ -91,14 +142,75 @@
 			<plugin>
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>${compiler-plugin.version}</version>
+				<configuration>
+					<annotationProcessorPaths>
+						<path>
+							<groupId>com.google.auto.value</groupId>
+							<artifactId>auto-value</artifactId>
+							<version>${auto-value.version}</version>
+						</path>
+					</annotationProcessorPaths>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>exec-maven-plugin</artifactId>
+				<version>1.3.2</version>
+				<executions>
+					<execution>
+						<id>npm install (initialize)</id>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<phase>initialize</phase>
+						<configuration>
+							<executable>npm</executable>
+							<arguments>
+								<argument>install</argument>
+								<argument>-f</argument>
+							</arguments>
+						</configuration>
+					</execution>
+					<execution>
+						<id>npm clean</id>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<phase>clean</phase>
+						<configuration>
+							<executable>npm</executable>
+							<arguments>
+								<argument>run</argument>
+								<argument>clean</argument>
+							</arguments>
+						</configuration>
+					</execution>
+					<execution>
+						<id>npm run pre-test</id>
+						<goals>
+							<goal>exec</goal>
+						</goals>
+						<phase>generate-test-resources</phase>
+						<configuration>
+							<executable>npm</executable>
+							<skip>${maven.test.skip}</skip>
+							<arguments>
+								<argument>run</argument>
+								<argument>generate-json-schema</argument>
+							</arguments>
+						</configuration>
+					</execution>
+				</executions>
 			</plugin>
 			<plugin>
 				<artifactId>maven-surefire-plugin</artifactId>
 				<version>${surefire-plugin.version}</version>
 				<configuration>
-					<systemProperties>
+					<skipTests>false</skipTests>
+					<systemPropertyVariables>
 						<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
-					</systemProperties>
+						<maven.home>${maven.home}</maven.home>
+					</systemPropertyVariables>
 				</configuration>
 			</plugin>
 		</plugins>
@@ -123,9 +235,11 @@
 									<goal>verify</goal>
 								</goals>
 								<configuration>
-									<systemProperties>
+									<systemPropertyVariables>
 										<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
-									</systemProperties>
+										<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
+										<maven.home>${maven.home}</maven.home>
+									</systemPropertyVariables>
 								</configuration>
 							</execution>
 						</executions>
@@ -136,50 +250,5 @@
 				<quarkus.package.type>native</quarkus.package.type>
 			</properties>
 		</profile>
-		<profile>
-			<id>sonar-dev</id>
-			<build>
-				<plugins>
-					<plugin>
-						<artifactId>maven-surefire-plugin</artifactId>
-						<version>${surefire-plugin.version}</version>
-					</plugin>
-					<plugin>
-						<groupId>org.jacoco</groupId>
-						<artifactId>jacoco-maven-plugin</artifactId>
-						<version>0.8.4</version>
-						<configuration>
-							<destFile>${sonar.jacoco.reportPaths}</destFile>
-							<append>true</append>
-						</configuration>
-						<executions>
-							<execution>
-								<goals>
-									<goal>prepare-agent</goal>
-								</goals>
-							</execution>
-							<execution>
-								<id>report</id>
-								<phase>test</phase>
-								<goals>
-									<goal>report</goal>
-								</goals>
-								<configuration>
-									<outputDirectory>${sonar.jacoco.reportPaths}</outputDirectory>
-								</configuration>
-							</execution>
-						</executions>
-					</plugin>
-					<plugin>
-						<groupId>org.sonarsource.scanner.maven</groupId>
-						<artifactId>sonar-maven-plugin</artifactId>
-						<version>3.6.0.1398</version>
-					</plugin>
-				</plugins>
-			</build>
-			<properties>
-				<sonar.host.url>https://sonarqube.dev.docker</sonar.host.url>
-			</properties>
-		</profile>
 	</profiles>
-</project>
+</project>
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
index e52c14a4..1aadfe55 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
@@ -12,7 +12,6 @@ package org.eclipsefoundation.git.eca.api;
 import java.util.List;
 
 import javax.ws.rs.GET;
-import javax.ws.rs.HeaderParam;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -21,6 +20,9 @@ import javax.ws.rs.QueryParam;
 import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
 
+import io.quarkus.oidc.client.filter.OidcClientFilter;
+import io.quarkus.security.Authenticated;
+
 /**
  * Binding interface for the Eclipse Foundation user account API. Runtime
  * implementations are automatically generated by Quarkus at compile time. As
@@ -30,7 +32,10 @@ import org.eclipsefoundation.git.eca.model.EclipseUser;
  * @author Martin Lowe
  *
  */
+@OidcClientFilter
+@Authenticated
 @RegisterRestClient
+@Produces("application/json")
 public interface AccountsAPI {
 
 	/**
@@ -43,8 +48,7 @@ public interface AccountsAPI {
 	 */
 	@GET
 	@Path("/account/profile")
-	@Produces("application/json")
-	List<EclipseUser> getUsers(@HeaderParam("Authorization") String authBearer, @QueryParam("uid") String id,
+	List<EclipseUser> getUsers(@QueryParam("uid") String id,
 			@QueryParam("name") String name, @QueryParam("mail") String mail);
 
 	/**
@@ -57,7 +61,6 @@ public interface AccountsAPI {
 	 */
 	@GET
 	@Path("/github/profile/{uname}")
-	@Produces("application/json")
-	EclipseUser getUserByGithubUname(@HeaderParam("Authorization") String authBearer, @PathParam("uname") String uname);
+	EclipseUser getUserByGithubUname(@PathParam("uname") String uname);
 
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/CustomJacksonConfig.java b/src/main/java/org/eclipsefoundation/git/eca/config/CustomJacksonConfig.java
deleted file mode 100644
index 567946ac..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/config/CustomJacksonConfig.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.config;
-
-import javax.inject.Singleton;
-
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import io.quarkus.jackson.ObjectMapperCustomizer;
-
-/**
- * Sets Jackson serializer to not fail when an unknown property is encountered.
- * 
- * @author Martin Lowe
- *
- */
-@Singleton
-public class CustomJacksonConfig implements ObjectMapperCustomizer {
-
-	@Override
-	public void customize(ObjectMapper objectMapper) {
-		objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/config/SecretConfigSource.java b/src/main/java/org/eclipsefoundation/git/eca/config/SecretConfigSource.java
deleted file mode 100644
index 956db868..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/config/SecretConfigSource.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.config;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Properties;
-import java.util.ServiceLoader;
-
-import org.eclipse.microprofile.config.spi.ConfigSource;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Reads in a set of secret configuration values from the secret.properties file
- * in the resources folder. These values are only secret in that they are set to
- * be ignored by Git and should be set on a per-server basis.
- * 
- * ConfigSource implementation was used to enable the usage of the
- * {@link ServiceLoader} to load the configurations.
- * 
- * @author Martin Lowe
- */
-public class SecretConfigSource implements ConfigSource {
-	private static final Logger LOGGER = LoggerFactory.getLogger(SecretConfigSource.class);
-
-	private static final String DEFAULT_SECRET_LOCATION = "/run/secrets/secret.properties";
-
-	private Map<String, String> secrets;
-
-	@Override
-	@SuppressWarnings({"rawtypes", "unchecked"})
-	public Map<String, String> getProperties() {
-		if (secrets == null) {
-			this.secrets = new HashMap<>();
-			String secretPath = System.getProperty("config.secret.path");
-			// Fallback to checking env if not set in JVM
-			if (secretPath == null || "".equals(secretPath.trim())) {
-				secretPath = System.getenv("CONFIG_SECRET_PATH");
-			}
-			if (secretPath == null || "".equals(secretPath.trim())) {
-				LOGGER.error(
-						"Configuration 'config.secret.path' and environment variable of 'CONFIG_SECRET_PATH' not set, using default value of "
-								+ DEFAULT_SECRET_LOCATION);
-				secretPath = DEFAULT_SECRET_LOCATION;
-			}
-			// load the secrets file in
-			File f = new File(secretPath);
-			if (!f.exists() || !f.canRead()) {
-				LOGGER.error("File at path {} either does not exist or cannot be read", secretPath);
-				return this.secrets;
-			}
-
-			// read each of the lines of secret config that should be added
-			try (BufferedReader br = new BufferedReader(new FileReader(f))) {
-				Properties p = new Properties();
-				p.load(br);
-				secrets.putAll((Map) p);
-
-			} catch (IOException e) {
-				LOGGER.error("Error while reading in secrets configuration file.", e);
-			}
-			LOGGER.debug("Found secret keys: {}", secrets.keySet());
-
-			// add priority ordinal to map if missing. 260 ordinal sets the priority between
-			// container and environment variable priority.
-			secrets.computeIfAbsent(ConfigSource.CONFIG_ORDINAL, key -> "260");
-		}
-		return secrets;
-	}
-
-	@Override
-	public String getValue(String propertyName) {
-		return getProperties().get(propertyName);
-	}
-
-	@Override
-	public String getName() {
-		return "secret";
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
index 506ea174..023dde77 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/Commit.java
@@ -12,138 +12,63 @@ package org.eclipsefoundation.git.eca.model;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
 /**
  * Represents a Git commit with basic data and metadata about the revision.
  * 
  * @author Martin Lowe
  *
  */
-public class Commit {
-	private String hash;
-	private String subject;
-	private String body;
-	private List<String> parents;
-	private GitUser author;
-	private GitUser committer;
-	private boolean head;
-
-	/**
-	 * @return the hash
-	 */
-	public String getHash() {
-		return hash;
-	}
+@AutoValue
+@JsonDeserialize(builder = AutoValue_Commit.Builder.class)
+public abstract class Commit {
+    @Nullable
+	public abstract String getHash();
 
-	/**
-	 * @param hash the hash to set
-	 */
-	public void setHash(String hash) {
-		this.hash = hash;
-	}
+    @Nullable
+	public abstract String getSubject();
 
-	/**
-	 * @return the subject
-	 */
-	public String getSubject() {
-		return subject;
-	}
+    @Nullable
+	public abstract String getBody();
 
-	/**
-	 * @param subject the subject to set
-	 */
-	public void setSubject(String subject) {
-		this.subject = subject;
-	}
+    @Nullable
+	public abstract List<String> getParents();
 
-	/**
-	 * @return the body
-	 */
-	public String getBody() {
-		return body;
-	}
+    @Nullable
+	public abstract GitUser getAuthor();
 
-	/**
-	 * @param body the body to set
-	 */
-	public void setBody(String body) {
-		this.body = body;
-	}
+    @Nullable
+	public abstract GitUser getCommitter();
 
-	/**
-	 * @return the parents
-	 */
-	public List<String> getParents() {
-		return new ArrayList<>(parents);
-	}
+    @Nullable
+	public abstract Boolean getHead();
 
-	/**
-	 * @param parents the parents to set
-	 */
-	public void setParents(List<String> parents) {
-		this.parents = new ArrayList<>(parents);
+	public static Builder builder() {
+		return new AutoValue_Commit.Builder().setParents(new ArrayList<>());
 	}
 
-	/**
-	 * @return the author
-	 */
-	public GitUser getAuthor() {
-		return author;
-	}
+	@AutoValue.Builder
+	@JsonPOJOBuilder(withPrefix = "set")
+	public abstract static class Builder {
+		public abstract Builder setHash(@Nullable String hash);
 
-	/**
-	 * @param author the author to set
-	 */
-	public void setAuthor(GitUser author) {
-		this.author = author;
-	}
+		public abstract Builder setSubject(@Nullable String subject);
 
-	/**
-	 * @return the commiter
-	 */
-	public GitUser getCommitter() {
-		return committer;
-	}
+		public abstract Builder setBody(@Nullable String body);
 
-	/**
-	 * @param committer the committer to set
-	 */
-	public void setCommitter(GitUser committer) {
-		this.committer = committer;
-	}
+		public abstract Builder setParents(@Nullable List<String> parents);
 
-	/**
-	 * @return the head
-	 */
-	public boolean isHead() {
-		return head;
-	}
+		public abstract Builder setAuthor(@Nullable GitUser author);
 
-	/**
-	 * @param head the head to set
-	 */
-	public void setHead(boolean head) {
-		this.head = head;
-	}
+		public abstract Builder setCommitter(@Nullable GitUser committer);
+
+		public abstract Builder setHead(@Nullable Boolean head);
 
-	@Override
-	public String toString() {
-		StringBuilder builder = new StringBuilder();
-		builder.append("Commit [hash=");
-		builder.append(hash);
-		builder.append(", subject=");
-		builder.append(subject);
-		builder.append(", body=");
-		builder.append(body);
-		builder.append(", parents=");
-		builder.append(parents);
-		builder.append(", author=");
-		builder.append(author);
-		builder.append(", committer=");
-		builder.append(committer);
-		builder.append(", head=");
-		builder.append(head);
-		builder.append("]");
-		return builder.toString();
+		public abstract Commit build();
 	}
-	
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java b/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java
index 7ddfd6ed..3fd94d51 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/CommitStatus.java
@@ -14,131 +14,79 @@ import java.util.List;
 
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
 /**
- * Contains information generated about a commit that was submitted for
- * validation to the API.
+ * Contains information generated about a commit that was submitted for validation to the API.
  * 
  * @author Martin Lowe
  *
  */
-public class CommitStatus {
-	private List<CommitStatusMessage> messages;
-	private List<CommitStatusMessage> warnings;
-	private List<CommitStatusMessage> errors;
-
-	public CommitStatus() {
-		this.messages = new ArrayList<>();
-		this.warnings = new ArrayList<>();
-		this.errors = new ArrayList<>();
-	}
-
-	/**
-	 * @return the msgs
-	 */
-	public List<CommitStatusMessage> getMessages() {
-		return new ArrayList<>(messages);
-	}
-
-	/**
-	 * @param messages the msgs to set
-	 */
-	public void setMessages(List<CommitStatusMessage> messages) {
-		this.messages = new ArrayList<>(messages);
-	}
-
-	/**
-	 * @param message message to add to current commit status
-	 * @param code    the status code for the message
-	 */
-	public void addMessage(String message, APIStatusCode code) {
-		this.messages.add(new CommitStatusMessage(code, message));
-	}
-
-	/**
-	 * @return the warnings
-	 */
-	public List<CommitStatusMessage> getWarnings() {
-		return new ArrayList<>(warnings);
-	}
-
-	/**
-	 * @param warnings the warnings to set
-	 */
-	public void setWarnings(List<CommitStatusMessage> warnings) {
-		this.warnings = new ArrayList<>(warnings);
-	}
-
-	/**
-	 * @param warning warning to add to current commit status
-	 * @param code    the status code for the message
-	 */
-	public void addWarning(String warning, APIStatusCode code) {
-		this.warnings.add(new CommitStatusMessage(code, warning));
-	}
-
-	/**
-	 * @return the errs
-	 */
-	public List<CommitStatusMessage> getErrors() {
-		return new ArrayList<>(errors);
-	}
-
-	/**
-	 * @param errors the errors to set
-	 */
-	public void setErrors(List<CommitStatusMessage> errors) {
-		this.errors = new ArrayList<>(errors);
-	}
-
-	/**
-	 * @param error error message to add to current commit status
-	 * @param code  the error status for the current message
-	 */
-	public void addError(String error, APIStatusCode code) {
-		this.errors.add(new CommitStatusMessage(code, error));
-	}
-
-	/**
-	 * Represents a message with an associated error or success status code.
-	 * 
-	 * @author Martin Lowe
-	 *
-	 */
-	public static class CommitStatusMessage {
-		private APIStatusCode code;
-		private String message;
-
-		public CommitStatusMessage(APIStatusCode code, String message) {
-			this.code = code;
-			this.message = message;
-		}
-
-		/**
-		 * @return the code
-		 */
-		public APIStatusCode getCode() {
-			return code;
-		}
-
-		/**
-		 * @param code the code to set
-		 */
-		public void setCode(APIStatusCode code) {
-			this.code = code;
-		}
-
-		/**
-		 * @return the message
-		 */
-		public String getMessage() {
-			return message;
-		}
-
-		/**
-		 * @param message the message to set
-		 */
-		public void setMessage(String message) {
-			this.message = message;
-		}
-	}
+@AutoValue
+@JsonDeserialize(builder = AutoValue_CommitStatus.Builder.class)
+public abstract class CommitStatus {
+    public abstract List<CommitStatusMessage> getMessages();
+
+    public abstract List<CommitStatusMessage> getWarnings();
+
+    public abstract List<CommitStatusMessage> getErrors();
+
+    public void addMessage(String message, APIStatusCode code) {
+        this.getMessages().add(CommitStatusMessage.builder().setCode(code).setMessage(message).build());
+    }
+
+    public void addWarning(String message, APIStatusCode code) {
+        this.getWarnings().add(CommitStatusMessage.builder().setCode(code).setMessage(message).build());
+    }
+
+    public void addError(String message, APIStatusCode code) {
+        this.getErrors().add(CommitStatusMessage.builder().setCode(code).setMessage(message).build());
+    }
+
+    public static Builder builder() {
+        return new AutoValue_CommitStatus.Builder().setErrors(new ArrayList<>()).setWarnings(new ArrayList<>())
+                .setMessages(new ArrayList<>());
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setMessages(List<CommitStatusMessage> messages);
+
+        public abstract Builder setWarnings(List<CommitStatusMessage> warnings);
+
+        public abstract Builder setErrors(List<CommitStatusMessage> errors);
+
+        public abstract CommitStatus build();
+    }
+
+    /**
+     * Represents a message with an associated error or success status code.
+     * 
+     * @author Martin Lowe
+     *
+     */
+    @AutoValue
+    @JsonDeserialize(builder = AutoValue_CommitStatus_CommitStatusMessage.Builder.class)
+    public abstract static class CommitStatusMessage {
+        public abstract APIStatusCode getCode();
+
+        public abstract String getMessage();
+
+        public static Builder builder() {
+            return new AutoValue_CommitStatus_CommitStatusMessage.Builder();
+        }
+
+        @AutoValue.Builder
+        @JsonPOJOBuilder(withPrefix = "set")
+        public abstract static class Builder {
+            public abstract Builder setCode(APIStatusCode code);
+
+            public abstract Builder setMessage(String message);
+
+            public abstract CommitStatusMessage build();
+        }
+    }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/EclipseUser.java b/src/main/java/org/eclipsefoundation/git/eca/model/EclipseUser.java
index 966ebf23..3d1dae57 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/EclipseUser.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/EclipseUser.java
@@ -8,154 +8,90 @@
  */
 package org.eclipsefoundation.git.eca.model;
 
+import javax.annotation.Nullable;
+
 import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.databind.PropertyNamingStrategy;
-import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
 
 /**
  * Represents a users Eclipse Foundation account
  *
  * @author Martin Lowe
  */
-public class EclipseUser {
-  private int uid;
-  private String name;
-  private String mail;
-  private ECA eca;
-  private boolean isCommitter;
-  // this field is internal for tracking bot stubs
-  @JsonIgnore private boolean isBot;
-
-  /**
-   * Create a bot user stub when there is no real Eclipse account for the bot.
-   *
-   * @param user the Git user that was detected to be a bot.
-   * @return a stubbed Eclipse user bot object.
-   */
-  public static EclipseUser createBotStub(GitUser user) {
-    EclipseUser stub = new EclipseUser();
-    stub.setName(user.getName());
-    stub.setMail(user.getMail());
-    stub.setEca(new ECA());
-    stub.setBot(true);
-    return stub;
-  }
-
-  /** @return the id */
-  public int getId() {
-    return uid;
-  }
-
-  /** @param id the id to set */
-  public void setId(int uid) {
-    this.uid = uid;
-  }
-
-  /** @return the name */
-  public String getName() {
-    return name;
-  }
-
-  /** @param name the name to set */
-  public void setName(String name) {
-    this.name = name;
-  }
-
-  /** @return the mail */
-  public String getMail() {
-    return mail;
-  }
-
-  /** @param mail the mail to set */
-  public void setMail(String mail) {
-    this.mail = mail;
-  }
-
-  /** @return the eca */
-  public ECA getEca() {
-    return eca;
-  }
-
-  /** @param eca the eca to set */
-  public void setEca(ECA eca) {
-    this.eca = eca;
-  }
-
-  /** @return the isCommitter */
-  public boolean isCommitter() {
-    return isCommitter;
-  }
-
-  /** @param isCommitter the isCommitter to set */
-  public void setCommitter(boolean isCommitter) {
-    this.isCommitter = isCommitter;
-  }
-
-  /** @return the isBot */
-  public boolean isBot() {
-    return isBot;
-  }
-
-  /** @param isBot the isBot to set */
-  private void setBot(boolean isBot) {
-    this.isBot = isBot;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    builder.append("EclipseUser [uid=");
-    builder.append(uid);
-    builder.append(", name=");
-    builder.append(name);
-    builder.append(", mail=");
-    builder.append(mail);
-    builder.append(", eca=");
-    builder.append(eca);
-    builder.append(", isCommitter=");
-    builder.append(isCommitter);
-    builder.append(", isBot=");
-    builder.append(isBot);
-    builder.append("]");
-    return builder.toString();
-  }
-
-  /**
-   * ECA for Eclipse accounts, representing whether users have signed the Eclipse Committer
-   * Agreement to enable contribution.
-   */
-  @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
-  public static class ECA {
-    private boolean signed;
-    private boolean canContributeSpecProject;
-
-    public ECA() {
-      this(false, false);
-    }
+@AutoValue
+@JsonDeserialize(builder = AutoValue_EclipseUser.Builder.class)
+public abstract class EclipseUser {
+    public abstract int getUid();
 
-    public ECA(boolean signed, boolean canContributeSpecProject) {
-      this.signed = signed;
-      this.canContributeSpecProject = canContributeSpecProject;
-    }
+    public abstract String getName();
+
+    public abstract String getMail();
+
+    public abstract ECA getECA();
 
-    /** @return the signed */
-    public boolean isSigned() {
-      return signed;
+    public abstract boolean getIsCommitter();
+
+    @Nullable
+    @JsonIgnore
+    public abstract Boolean getIsBot();
+
+    /**
+     * Create a bot user stub when there is no real Eclipse account for the bot.
+     *
+     * @param user the Git user that was detected to be a bot.
+     * @return a stubbed Eclipse user bot object.
+     */
+    public static EclipseUser createBotStub(GitUser user) {
+        return EclipseUser.builder().setUid(0).setName(user.getName()).setMail(user.getMail())
+                .setECA(ECA.builder().build()).setIsBot(true).build();
     }
 
-    /** @param signed the signed to set */
-    public void setSigned(boolean signed) {
-      this.signed = signed;
+    public static Builder builder() {
+        return new AutoValue_EclipseUser.Builder().setIsCommitter(false).setIsBot(false);
     }
 
-    /** @return the canContributeSpecProject */
-    public boolean isCanContributeSpecProject() {
-      return canContributeSpecProject;
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setUid(int id);
+
+        public abstract Builder setName(String name);
+
+        public abstract Builder setMail(String mail);
+
+        public abstract Builder setECA(ECA eca);
+
+        public abstract Builder setIsCommitter(boolean isCommitter);
+
+        @JsonIgnore
+        public abstract Builder setIsBot(@Nullable Boolean isBot);
+
+        public abstract EclipseUser build();
     }
 
-    /** @param canContributeSpecProject the canContributeSpecProject to set */
-    public void setCanContributeSpecProject(boolean canContributeSpecProject) {
-      this.canContributeSpecProject = canContributeSpecProject;
+    @AutoValue
+    @JsonDeserialize(builder = AutoValue_EclipseUser_ECA.Builder.class)
+    public abstract static class ECA {
+        @Nullable
+        public abstract Boolean getSigned();
+
+        @Nullable
+        public abstract Boolean getCanContributeSpecProject();
+
+        public static Builder builder() {
+            return new AutoValue_EclipseUser_ECA.Builder().setCanContributeSpecProject(false).setSigned(false);
+        }
+
+        @AutoValue.Builder
+        @JsonPOJOBuilder(withPrefix = "set")
+        public abstract static class Builder {
+            public abstract Builder setSigned(@Nullable Boolean signed);
+
+            public abstract Builder setCanContributeSpecProject(@Nullable Boolean canContributeSpecProject);
+
+            public abstract ECA build();
+        }
     }
-  }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/GitUser.java b/src/main/java/org/eclipsefoundation/git/eca/model/GitUser.java
index b868ead5..9dd19392 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/GitUser.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/GitUser.java
@@ -9,41 +9,38 @@
  ******************************************************************************/
 package org.eclipsefoundation.git.eca.model;
 
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
 /**
  * Basic object representing a Git users data required for verification.
  * 
  * @author Martin Lowe
  *
  */
-public class GitUser {
-	private String name;
-	private String mail;
-
-	/**
-	 * @return the name
-	 */
-	public String getName() {
-		return name;
-	}
-
-	/**
-	 * @param name the name to set
-	 */
-	public void setName(String name) {
-		this.name = name;
-	}
-
-	/**
-	 * @return the mail
-	 */
-	public String getMail() {
-		return mail;
-	}
-
-	/**
-	 * @param mail the mail to set
-	 */
-	public void setMail(String mail) {
-		this.mail = mail;
-	}
-}
+@AutoValue
+@JsonDeserialize(builder = AutoValue_GitUser.Builder.class)
+public abstract class GitUser {
+    @Nullable
+    public abstract String getName();
+
+    @Nullable
+    public abstract String getMail();
+
+    public static Builder builder() {
+        return new AutoValue_GitUser.Builder();
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setName(@Nullable String name);
+
+        public abstract Builder setMail(@Nullable String mail);
+
+        public abstract GitUser build();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/Project.java b/src/main/java/org/eclipsefoundation/git/eca/model/Project.java
index b72d397d..2131223b 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/Project.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/Project.java
@@ -13,212 +13,120 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.PropertyNamingStrategy;
-import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
 
 /**
- * Represents a project in the Eclipse API, along with the users and repos that
- * exist within the context of the project.
+ * Represents a project in the Eclipse API, along with the users and repos that exist within the context of the project.
  * 
  * @author Martin Lowe
  *
  */
-@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
-public class Project {
-	private String projectId;
-	private String name;
-	private List<User> committers;
-	private List<Repo> repos;
-	private List<Repo> gitlabRepos;
-	private List<Repo> githubRepos;
-	private List<Repo> gerritRepos;
-	private String specWorkingGroup;
-	
-	public Project() {
-		this.committers = new ArrayList<>();
-		this.repos = new ArrayList<>();
-		this.gitlabRepos = new ArrayList<>();
-		this.githubRepos = new ArrayList<>();
-		this.gerritRepos = new ArrayList<>();
-	}
-
-	/**
-	 * @return the projectId
-	 */
-	public String getProjectId() {
-		return projectId;
-	}
-
-	/**
-	 * @param projectId the projectId to set
-	 */
-	public void setProjectId(String projectId) {
-		this.projectId = projectId;
-	}
-
-	/**
-	 * @return the name
-	 */
-	public String getName() {
-		return name;
-	}
-
-	/**
-	 * @param name the name to set
-	 */
-	public void setName(String name) {
-		this.name = name;
-	}
-
-	/**
-	 * @return the committers
-	 */
-	public List<User> getCommitters() {
-		return committers;
-	}
-
-	/**
-	 * @param committers the committers to set
-	 */
-	public void setCommitters(List<User> committers) {
-		this.committers = committers;
-	}
-
-	/**
-	 * @return the repos
-	 */
-	public List<Repo> getRepos() {
-		return new ArrayList<>(repos);
-	}
-
-	/**
-	 * @param repos the repos to set
-	 */
-	public void setRepos(List<Repo> repos) {
-		this.repos = new ArrayList<>(repos);
-	}
-
-	/**
-	 * @return the gitlabRepos
-	 */
-	public List<Repo> getGitlabRepos() {
-		return new ArrayList<>(gitlabRepos);
-	}
-
-	/**
-	 * @param githubRepos the githubRepos to set
-	 */
-	public void setGithubRepos(List<Repo> githubRepos) {
-		this.githubRepos = new ArrayList<>(githubRepos);
-	}
-
-	/**
-	 * @return the gitlabRepos
-	 */
-	public List<Repo> getGithubRepos() {
-		return new ArrayList<>(githubRepos);
-	}
-
-	/**
-	 * @param gitlabRepos the gitlabRepos to set
-	 */
-	public void setGitlabRepos(List<Repo> gitlabRepos) {
-		this.gitlabRepos = new ArrayList<>(gitlabRepos);
-	}
-
-	/**
-   * @return the gerritRepos
-   */
-  public List<Repo> getGerritRepos() {
-    return new ArrayList<>(gerritRepos);
-  }
-
-  /**
-   * @param gerritRepos the gerritRepos to set
-   */
-  public void setGerritRepos(List<Repo> gerritRepos) {
-    this.gerritRepos = new ArrayList<>(gerritRepos);
-  }
-
-  /**
-	 * @return the specWorkingGroup
-	 */
-	public String getSpecWorkingGroup() {
-		return specWorkingGroup;
-	}
-
-	/**
-	 * @param specWorkingGroup the specWorkingGroup to set
-	 */
-	public void setSpecWorkingGroup(String specWorkingGroup) {
-		this.specWorkingGroup = specWorkingGroup;
-	}
-
-	/**
-	 * @param specProjectWorkingGroup the value for the spec_project_working_group.
-	 *                                When empty from API, represented by an empty
-	 *                                array, and a map/object when set.
-	 */
-	@SuppressWarnings("unchecked")
-	@JsonProperty("spec_project_working_group")
-	private void unpackSpecProject(Object specProjectWorkingGroup) {
-		if (specProjectWorkingGroup instanceof Map) {
-			Object raw = ((Map<String, Object>) specProjectWorkingGroup).get("id");
-			if (raw instanceof String) {
-				this.specWorkingGroup = (String) raw;
-			}
-		}
-	}
-
-	public static class Repo {
-		private String url;
-
-		/**
-		 * @return the url
-		 */
-		public String getUrl() {
-			return url;
-		}
-
-		/**
-		 * @param url the url to set
-		 */
-		public void setUrl(String url) {
-			this.url = url;
-		}
-	}
-
-	public static class User {
-		private String username;
-		private String url;
-
-		/**
-		 * @return the username
-		 */
-		public String getUsername() {
-			return username;
-		}
-
-		/**
-		 * @param username the username to set
-		 */
-		public void setUsername(String username) {
-			this.username = username;
-		}
-
-		/**
-		 * @return the url
-		 */
-		public String getUrl() {
-			return url;
-		}
-
-		/**
-		 * @param url the url to set
-		 */
-		public void setUrl(String url) {
-			this.url = url;
-		}
-	}
+@AutoValue
+@JsonDeserialize(builder = AutoValue_Project.Builder.class)
+public abstract class Project {
+    public abstract String getProjectId();
+
+    public abstract String getName();
+
+    public abstract List<User> getCommitters();
+
+    @Nullable
+    public abstract List<Repo> getRepos();
+
+    public abstract List<Repo> getGitlabRepos();
+
+    public abstract List<Repo> getGithubRepos();
+
+    public abstract List<Repo> getGerritRepos();
+
+    public abstract Object getSpecProjectWorkingGroup();
+
+    @Nullable
+    @Memoized
+    public String getSpecWorkingGroup() {
+        // stored as map as empty returns an array instead of a map
+        Object specProjectWorkingGroup = getSpecProjectWorkingGroup();
+        if (specProjectWorkingGroup instanceof Map) {
+            // we checked in line above that the map exists, so we can safely cast it
+            @SuppressWarnings("unchecked")
+            Object raw = ((Map<Object, Object>) specProjectWorkingGroup).get("id");
+            if (raw instanceof String) {
+                return (String) raw;
+            }
+        }
+        return null;
+    }
+
+    public static Builder builder() {
+        // adds empty lists as default values
+        return new AutoValue_Project.Builder().setRepos(new ArrayList<>()).setCommitters(new ArrayList<>())
+                .setGithubRepos(new ArrayList<>()).setGitlabRepos(new ArrayList<>()).setGerritRepos(new ArrayList<>());
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setProjectId(String projectId);
+
+        public abstract Builder setName(String name);
+
+        public abstract Builder setCommitters(List<User> committers);
+
+        public abstract Builder setRepos(@Nullable List<Repo> repos);
+
+        public abstract Builder setGitlabRepos(List<Repo> gitlabRepos);
+
+        public abstract Builder setGithubRepos(List<Repo> githubRepos);
+
+        public abstract Builder setGerritRepos(List<Repo> gerritRepos);
+
+        public abstract Builder setSpecProjectWorkingGroup(Object specProjectWorkingGroup);
+
+        public abstract Project build();
+    }
+
+    @AutoValue
+    @JsonDeserialize(builder = AutoValue_Project_User.Builder.class)
+    public abstract static class User {
+        public abstract String getUsername();
+
+        public abstract String getUrl();
+
+        public static Builder builder() {
+            return new AutoValue_Project_User.Builder();
+        }
+
+        @AutoValue.Builder
+        @JsonPOJOBuilder(withPrefix = "set")
+        public abstract static class Builder {
+            public abstract Builder setUsername(String username);
+
+            public abstract Builder setUrl(String url);
+
+            public abstract User build();
+        }
+    }
+
+    /**
+     * Does not use autovalue as the value should be mutable.
+     * 
+     * @author Martin Lowe
+     *
+     */
+    public static class Repo {
+        private String url;
+
+        public String getUrl() {
+            return this.url;
+        }
+
+        public void setUrl(String url) {
+            this.url = url;
+        }
+    }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java
index 14f1b4e4..ca946c1f 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationRequest.java
@@ -1,82 +1,66 @@
-/**
- * ***************************************************************************** Copyright (C) 2020
+/** ***************************************************************************** Copyright (C) 2020
  * Eclipse Foundation
  *
  * <p>This program and the accompanying materials are made available under the terms of the Eclipse
  * Public License 2.0 which is available at https://www.eclipse.org/legal/epl-2.0/
  *
  * <p>SPDX-License-Identifier: EPL-2.0
- * ****************************************************************************
- */
+ * *****************************************************************************/
 package org.eclipsefoundation.git.eca.model;
 
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+
 /**
  * Represents a request to validate a list of commits.
  *
  * @author Martin Lowe
  */
-public class ValidationRequest {
-  private URI repoUrl;
-  private List<Commit> commits;
-  private ProviderType provider;
-  private boolean strictMode;
+@AutoValue
+@JsonNaming(LowerCamelCaseStrategy.class)
+@JsonDeserialize(builder = AutoValue_ValidationRequest.Builder.class)
+public abstract class ValidationRequest {
+    @Nullable
+    @JsonProperty("repoUrl")
+    public abstract URI getRepoUrl();
 
-  /** @return the repoUrl */
-  public URI getRepoUrl() {
-    return repoUrl;
-  }
+    public abstract List<Commit> getCommits();
 
-  /** @param repoUrl the repoUrl to set */
-  public void setRepoUrl(URI repoUrl) {
-    this.repoUrl = repoUrl;
-  }
+    public abstract ProviderType getProvider();
 
-  /** @return the commits */
-  public List<Commit> getCommits() {
-    return new ArrayList<>(commits);
-  }
+    @Nullable
+    @JsonProperty("strictMode")
+    public abstract Boolean getStrictMode();
 
-  /** @param commits the commits to set */
-  public void setCommits(List<Commit> commits) {
-    this.commits = new ArrayList<>(commits);
-  }
+    public static Builder builder() {
+        return new AutoValue_ValidationRequest.Builder().setStrictMode(false).setCommits(new ArrayList<>());
+    }
 
-  /** @return the provider */
-  public ProviderType getProvider() {
-    return provider;
-  }
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        @JsonProperty("repoUrl")
+        public abstract Builder setRepoUrl(@Nullable URI repoUrl);
 
-  /** @param provider the provider to set */
-  public void setProvider(ProviderType provider) {
-    this.provider = provider;
-  }
+        public abstract Builder setCommits(List<Commit> commits);
 
-  /** @return the strictMode */
-  public boolean isStrictMode() {
-    return strictMode;
-  }
+        public abstract Builder setProvider(ProviderType provider);
 
-  /** @param strictMode the strictMode to set */
-  public void setStrictMode(boolean strictMode) {
-    this.strictMode = strictMode;
-  }
+        @JsonProperty("strictMode")
+        public abstract Builder setStrictMode(@Nullable Boolean strictMode);
 
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    builder.append("ValidationRequest [repoUrl=");
-    builder.append(repoUrl);
-    builder.append(", commits=");
-    builder.append(commits);
-    builder.append(", provider=");
-    builder.append(provider);
-    builder.append("]");
-    return builder.toString();
-  }
+        public abstract ValidationRequest build();
+    }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
index 6348452b..1071df85 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/model/ValidationResponse.java
@@ -10,7 +10,7 @@
  */
 package org.eclipsefoundation.git.eca.model;
 
-import java.util.Date;
+import java.time.ZonedDateTime;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -19,117 +19,90 @@ import javax.ws.rs.core.Response.Status;
 
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 
+import com.fasterxml.jackson.databind.PropertyNamingStrategies.LowerCamelCaseStrategy;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.auto.value.AutoValue;
+import com.google.auto.value.extension.memoized.Memoized;
+
 /**
  * Represents an internal response for a call to this API.
  *
  * @author Martin Lowe
  */
-public class ValidationResponse {
-  private boolean passed;
-  private int errorCount;
-  private Date time;
-  private Map<String, CommitStatus> commits;
-  private boolean trackedProject;
-  private boolean strictMode;
-
-  public ValidationResponse() {
-    this.commits = new HashMap<>();
-    this.time = new Date();
-    this.strictMode = false;
-  }
-
-  /** @return the passed */
-  public boolean isPassed() {
-    return passed;
-  }
-
-  /** @param passed the passed to set */
-  public void setPassed(boolean passed) {
-    this.passed = passed;
-  }
-
-  /** @return the errorCount */
-  public int getErrorCount() {
-    this.errorCount = commits.values().stream().mapToInt(s -> s.getErrors().size()).sum();
-    return errorCount;
-  }
-
-  /** @param errorCount the errorCount to set */
-  public void setErrorCount(int errorCount) {
-    this.errorCount = errorCount;
-  }
-
-  /** @return the time */
-  public Date getTime() {
-    return time;
-  }
-
-  /** @param time the time to set */
-  public void setTime(Date time) {
-    this.time = time;
-  }
-
-  /** @return the commits */
-  public Map<String, CommitStatus> getCommits() {
-    return commits;
-  }
-
-  /** @param commits the commits to set */
-  public void setCommits(Map<String, CommitStatus> commits) {
-    this.commits = commits;
-  }
-
-  /** @param message message to add to the API response */
-  public void addMessage(String hash, String message, APIStatusCode code) {
-    commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addMessage(message, code);
-  }
-
-  /** @param warning message to add to the API response */
-  public void addWarning(String hash, String warning, APIStatusCode code) {
-    commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addWarning(warning, code);
-  }
-
-  /** @param error message to add to the API response */
-  public void addError(String hash, String error, APIStatusCode code) {
-    commits.computeIfAbsent(getHashKey(hash), k -> new CommitStatus()).addError(error, code);
-  }
-
-  /** @return the trackedProject */
-  public boolean isTrackedProject() {
-    return trackedProject;
-  }
-
-  /** @param trackedProject the trackedProject to set */
-  public void setTrackedProject(boolean trackedProject) {
-    this.trackedProject = trackedProject;
-  }
-
-  /** @return the strictMode */
-  public boolean isStrictMode() {
-    return strictMode;
-  }
-
-  /** @param strictMode the strictMode to set */
-  public void setStrictMode(boolean strictMode) {
-    this.strictMode = strictMode;
-  }
-
-  private String getHashKey(String hash) {
-    return hash == null ? "_nil" : hash;
-  }
-
-  /**
-   * Converts the APIResponse to a web response with appropriate status.
-   *
-   * @return a web response with status {@link Status.OK} if the commits pass validation, {@link
-   *     Status.FORBIDDEN} otherwise.
-   */
-  public Response toResponse() {
-    // update error count before returning
-    if (passed) {
-      return Response.ok(this).build();
-    } else {
-      return Response.status(Status.FORBIDDEN).entity(this).build();
+@AutoValue
+@JsonNaming(LowerCamelCaseStrategy.class)
+@JsonDeserialize(builder = AutoValue_ValidationResponse.Builder.class)
+public abstract class ValidationResponse {
+
+    public abstract ZonedDateTime getTime();
+
+    public abstract Map<String, CommitStatus> getCommits();
+
+    public abstract boolean getTrackedProject();
+
+    public abstract boolean getStrictMode();
+
+    public boolean getPassed() {
+        return getErrorCount() <= 0;
+    };
+
+    @Memoized
+    public int getErrorCount() {
+        return getCommits().values().stream().mapToInt(s -> s.getErrors().size()).sum();
+    }
+
+    /** @param message message to add to the API response */
+    public void addMessage(String hash, String message, APIStatusCode code) {
+        getCommits().computeIfAbsent(getHashKey(hash), k -> CommitStatus.builder().build()).addMessage(message, code);
+    }
+
+    /** @param warning message to add to the API response */
+    public void addWarning(String hash, String warning, APIStatusCode code) {
+        getCommits().computeIfAbsent(getHashKey(hash), k -> CommitStatus.builder().build()).addWarning(warning, code);
+    }
+
+    /** @param error message to add to the API response */
+    public void addError(String hash, String error, APIStatusCode code) {
+        getCommits().computeIfAbsent(getHashKey(hash), k -> CommitStatus.builder().build()).addError(error, code);
+    }
+
+    private String getHashKey(String hash) {
+        return hash == null ? "_nil" : hash;
+    }
+
+    /**
+     * Converts the APIResponse to a web response with appropriate status.
+     *
+     * @return a web response with status {@link Status.OK} if the commits pass validation, {@link Status.FORBIDDEN}
+     * otherwise.
+     */
+    public Response toResponse() {
+        // update error count before returning
+        if (getPassed()) {
+            return Response.ok(this).build();
+        } else {
+            return Response.status(Status.FORBIDDEN).entity(this).build();
+        }
+    }
+
+    public static Builder builder() {
+        return new AutoValue_ValidationResponse.Builder().setStrictMode(false).setTrackedProject(false)
+                .setTime(ZonedDateTime.now()).setCommits(new HashMap<>());
+    }
+
+    @AutoValue.Builder
+    @JsonPOJOBuilder(withPrefix = "set")
+    public abstract static class Builder {
+        public abstract Builder setTime(ZonedDateTime time);
+
+        public abstract Builder setCommits(Map<String, CommitStatus> commits);
+
+        public abstract Builder setTrackedProject(boolean trackedProject);
+
+        public abstract Builder setStrictMode(boolean strictMode);
+
+        public abstract ValidationResponse build();
     }
-  }
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java b/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
deleted file mode 100644
index 3e48dbb8..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.oauth;
-
-import com.github.scribejava.core.builder.api.DefaultApi20;
-import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
-import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme;
-
-/**
- * Wrapper around the OAuth API for Scribejava. Enables OAuth2.0 binding to the
- * Eclipse Foundation OAuth server.
- * 
- * @author Martin Lowe
- *
- */
-public class EclipseApi extends DefaultApi20 {
-
-	@Override
-	public String getAccessTokenEndpoint() {
-		return "https://accounts.eclipse.org/oauth2/token";
-	}
-
-	@Override
-	protected String getAuthorizationBaseUrl() {
-		return null;
-	}
-
-	@Override
-	public ClientAuthentication getClientAuthentication() {
-		return RequestBodyAuthenticationScheme.instance();
-	}
-
-	private static class InstanceHolder {
-		private static final EclipseApi INSTANCE = new EclipseApi();
-	}
-
-	public static EclipseApi instance() {
-		return InstanceHolder.INSTANCE;
-	}
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
index aa669a93..8bae1e5c 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -9,6 +9,7 @@
 package org.eclipsefoundation.git.eca.resource;
 
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
@@ -29,6 +30,7 @@ import javax.ws.rs.core.Response;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.AccountsAPI;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
@@ -40,25 +42,27 @@ import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
-import org.eclipsefoundation.git.eca.service.CachingService;
-import org.eclipsefoundation.git.eca.service.OAuthService;
 import org.eclipsefoundation.git.eca.service.ProjectsService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
 /**
- * ECA validation endpoint for Git commits. Will use information from the bots, projects, and
- * accounts API to validate commits passed to this endpoint. Should be as system agnostic as
- * possible to allow for any service to request validation with less reliance on services external
+ * ECA validation endpoint for Git commits. Will use information from the bots,
+ * projects, and
+ * accounts API to validate commits passed to this endpoint. Should be as system
+ * agnostic as
+ * possible to allow for any service to request validation with less reliance on
+ * services external
  * to the Eclipse foundation.
  *
  * @author Martin Lowe
  */
 @Path("/eca")
-@Consumes({MediaType.APPLICATION_JSON})
-@Produces({MediaType.APPLICATION_JSON})
+@Consumes({ MediaType.APPLICATION_JSON })
+@Produces({ MediaType.APPLICATION_JSON })
 public class ValidationResource {
   private static final Logger LOGGER = LoggerFactory.getLogger(ValidationResource.class);
 
@@ -70,13 +74,18 @@ public class ValidationResource {
   List<String> emailPatterns;
 
   // eclipse API rest client interfaces
-  @Inject @RestClient AccountsAPI accounts;
-  @Inject @RestClient BotsAPI bots;
+  @Inject
+  @RestClient
+  AccountsAPI accounts;
+  @Inject
+  @RestClient
+  BotsAPI bots;
 
   // external API/service harnesses
-  @Inject OAuthService oauth;
-  @Inject CachingService cache;
-  @Inject ProjectsService projects;
+  @Inject
+  CachingService cache;
+  @Inject
+  ProjectsService projects;
 
   // rendered list of regex values
   List<Pattern> patterns;
@@ -88,40 +97,31 @@ public class ValidationResource {
   }
 
   /**
-   * Consuming a JSON request, this method will validate all passed commits, using the repo URL and
-   * the repository provider. These commits will be validated to ensure that all users are covered
-   * either by an ECA, or are committers on the project. In the case of ECA-only contributors, an
+   * Consuming a JSON request, this method will validate all passed commits, using
+   * the repo URL and
+   * the repository provider. These commits will be validated to ensure that all
+   * users are covered
+   * either by an ECA, or are committers on the project. In the case of ECA-only
+   * contributors, an
    * additional sign off footer is required in the body of the commit.
    *
    * @param req the request containing basic data plus the commits to be validated
-   * @return a web response indicating success or failure for each commit, along with standard
-   *     messages that may be used to give users context on failure.
+   * @return a web response indicating success or failure for each commit, along
+   *         with standard
+   *         messages that may be used to give users context on failure.
    * @throws MalformedURLException
    */
   @POST
   public Response validate(ValidationRequest req) {
-    ValidationResponse r = new ValidationResponse();
-    r.setStrictMode(req.isStrictMode());
-    // check that we have commits to validate
-    if (req.getCommits() == null || req.getCommits().isEmpty()) {
-      addError(r, "A commit is required to validate", null);
-    }
-    // check that we have a repo set
-    if (req.getRepoUrl() == null) {
-      addError(r, "A base repo URL needs to be set in order to validate", null);
-    }
-    // check that we have a type set
-    if (req.getProvider() == null) {
-      addError(r, "A provider needs to be set to validate a request", null);
-    }
+    List<String> messages = checkRequest(req);
     // only process if we have no errors
-    if (r.getErrorCount() == 0) {
+    if (messages.isEmpty()) {
       LOGGER.debug("Processing: {}", req);
       // filter the projects based on the repo URL. At least one repo in project must
       // match the repo URL to be valid
       List<Project> filteredProjects = retrieveProjectsForRequest(req);
-      // set whether this call has tracked projects
-      r.setTrackedProject(!filteredProjects.isEmpty());
+      ValidationResponse r = ValidationResponse.builder().setStrictMode(req.getStrictMode() != null && req.getStrictMode() ? true : false)
+              .setTrackedProject(!filteredProjects.isEmpty()).build();
       for (Commit c : req.getCommits()) {
         // process the request, capturing if we should continue processing
         boolean continueProcessing = processCommit(c, r, filteredProjects, req.getProvider());
@@ -130,21 +130,48 @@ public class ValidationResource {
           break;
         }
       }
+      return r.toResponse();
+    } else {
+        // create a stubbed response with the errors
+        ValidationResponse out = ValidationResponse.builder().build();
+        messages.forEach(m -> addError(out, m, null));
+        return out.toResponse();
     }
-    // depending on number of errors found, set response status
-    if (r.getErrorCount() == 0) {
-      r.setPassed(true);
-    }
-    return r.toResponse();
+  }
+  
+  /**
+   * Check if there are any issues with the validation request, returning error messages if there are issues with the
+   * request.
+   * 
+   * @param req the current validation request
+   * @return a list of error messages to report, or an empty list if there are no errors with the request.
+   */
+  private List<String> checkRequest(ValidationRequest req) {
+      // check that we have commits to validate
+      List<String> messages = new ArrayList<>();
+      if (req.getCommits() == null || req.getCommits().isEmpty()) {
+          messages.add("A commit is required to validate");
+      }
+      // check that we have a repo set
+      if (req.getRepoUrl() == null) {
+          messages.add("A base repo URL needs to be set in order to validate");
+      }
+      // check that we have a type set
+      if (req.getProvider() == null) {
+          messages.add("A provider needs to be set to validate a request");
+      }
+      return messages;
   }
 
   /**
-   * Process the current request, validating that the passed commit is valid. The author and
-   * committers Eclipse Account is retrieved, which are then used to check if the current commit is
+   * Process the current request, validating that the passed commit is valid. The
+   * author and
+   * committers Eclipse Account is retrieved, which are then used to check if the
+   * current commit is
    * valid for the current project.
    *
-   * @param c the commit to process
-   * @param response the response container
+   * @param c                the commit to process
+   * @param response         the response container
    * @param filteredProjects tracked projects for the current request
    * @return true if we should continue processing, false otherwise.
    */
@@ -233,76 +260,92 @@ public class ValidationResource {
     return true;
   }
 
-
-
   /**
-   * Validates author access for the current commit. If there are errors, they are recorded in the
-   * response for the current request to be returned once all validation checks are completed.
+   * Validates author access for the current commit. If there are errors, they are
+   * recorded in the
+   * response for the current request to be returned once all validation checks
+   * are completed.
    *
-   * @param r the current response object for the request
-   * @param c the commit that is being validated
-   * @param eclipseUser the user to validate on a branch
+   * @param r                the current response object for the request
+   * @param c                the commit that is being validated
+   * @param eclipseUser      the user to validate on a branch
    * @param filteredProjects tracked projects for the current request
-   * @param errorCode the error code to display if the user does not have access
+   * @param errorCode        the error code to display if the user does not have
+   *                         access
    */
   private void validateUserAccess(
       ValidationResponse r,
       Commit c,
       EclipseUser eclipseUser,
       List<Project> filteredProjects, APIStatusCode errorCode) {
-    // call isCommitter inline and pass to partial call 
+    // call isCommitter inline and pass to partial call
     validateUserAccessPartial(r, c, eclipseUser, isCommitter(r, eclipseUser, c.getHash(), filteredProjects), errorCode);
   }
 
   /**
-   * Allows for isCommitter to be called external to this method. This was extracted to ensure that isCommitter isn't 
-   * called twice for the same user when checking committer proxy push rules and committer general access.
+   * Allows for isCommitter to be called external to this method. This was
+   * extracted to ensure that isCommitter isn't
+   * called twice for the same user when checking committer proxy push rules and
+   * committer general access.
    * 
-   * @param r the current response object for the request
-   * @param c the commit that is being validated
+   * @param r           the current response object for the request
+   * @param c           the commit that is being validated
    * @param eclipseUser the user to validate on a branch
    * @param isCommitter the results of the isCommitter call from this class.
-   * @param errorCode the error code to display if the user does not have access
+   * @param errorCode   the error code to display if the user does not have access
    */
-  private void validateUserAccessPartial(ValidationResponse r, Commit c, EclipseUser eclipseUser, 
-        boolean isCommitter, APIStatusCode errorCode) {
+  private void validateUserAccessPartial(ValidationResponse r, Commit c, EclipseUser eclipseUser,
+      boolean isCommitter, APIStatusCode errorCode) {
     String userType = "author";
     if (APIStatusCode.ERROR_COMMITTER.equals(errorCode)) {
       userType = "committer";
     }
     if (isCommitter) {
-      addMessage(r, String.format("Eclipse user '%s'(%s) is a committer on the project.", eclipseUser.getName(), userType), c.getHash());
+      addMessage(r,
+          String.format("Eclipse user '%s'(%s) is a committer on the project.", eclipseUser.getName(), userType),
+          c.getHash());
     } else {
-      addMessage(r, String.format("Eclipse user '%s'(%s) is not a committer on the project.", eclipseUser.getName(), userType), c.getHash());
+      addMessage(r,
+          String.format("Eclipse user '%s'(%s) is not a committer on the project.", eclipseUser.getName(), userType),
+          c.getHash());
       // check if the author is signed off if not a committer
-      if (eclipseUser.getEca().isSigned()) {
+      if (eclipseUser.getECA().getSigned()) {
         addMessage(
             r,
-            String.format("Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.", eclipseUser.getName(), userType),
+            String.format("Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.",
+                eclipseUser.getName(), userType),
             c.getHash());
       } else {
         addMessage(
             r,
             String.format("Eclipse user '%s'(%s) does not have a current Eclipse Contributor Agreement (ECA) on file.\n"
-                + "If there are multiple commits, please ensure that each author has a ECA.", eclipseUser.getName(), userType),
+                + "If there are multiple commits, please ensure that each author has a ECA.", eclipseUser.getName(),
+                userType),
             c.getHash());
-        addError(r, String.format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).", eclipseUser.getName(), userType),
+        addError(r,
+            String.format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).",
+                eclipseUser.getName(), userType),
             c.getHash(), errorCode);
       }
     }
   }
 
   /**
-   * Checks whether the given user is a committer on the project. If they are and the project is
-   * also a specification for a working group, an additional access check is made against the user.
+   * Checks whether the given user is a committer on the project. If they are and
+   * the project is
+   * also a specification for a working group, an additional access check is made
+   * against the user.
    *
-   * <p>Additionally, a check is made to see if the user is a registered bot user for the given
-   * project. If they match for the given project, they are granted committer-like access to the
+   * <p>
+   * Additionally, a check is made to see if the user is a registered bot user for
+   * the given
+   * project. If they match for the given project, they are granted committer-like
+   * access to the
    * repository.
    *
-   * @param r the current response object for the request
-   * @param user the user to validate on a branch
-   * @param hash the hash of the commit that is being validated
+   * @param r                the current response object for the request
+   * @param user             the user to validate on a branch
+   * @param hash             the hash of the commit that is being validated
    * @param filteredProjects tracked projects for the current request
    * @return true if user is considered a committer, false otherwise.
    */
@@ -318,7 +361,7 @@ public class ValidationResource {
       if (p.getCommitters().stream().anyMatch(u -> u.getUsername().equals(user.getName()))) {
         // check if the current project is a committer project, and if the user can
         // commit to specs
-        if (p.getSpecWorkingGroup() != null && !user.getEca().isCanContributeSpecProject()) {
+        if (p.getSpecWorkingGroup() != null && !user.getECA().getCanContributeSpecProject()) {
           // set error + update response status
           r.addError(
               hash,
@@ -336,78 +379,85 @@ public class ValidationResource {
         }
       }
     }
-    // check if user is a bot, either through early detection or through on-demand check
-    if (user.isBot() || userIsABot(user.getMail(), filteredProjects)) {
+    // check if user is a bot, either through early detection or through on-demand
+    // check
+    if ((user.getIsBot() != null && user.getIsBot()) || userIsABot(user.getMail(), filteredProjects)) {
       LOGGER.debug("User '{} <{}>' was found to be a bot", user.getName(), user.getMail());
       return true;
     }
     return false;
   }
 
-    private boolean userIsABot(String mail, List<Project> filteredProjects) {
-        if (mail == null || "".equals(mail.trim())) {
-            return false;
-        }
-        List<JsonNode> botObjs = getBots();
-        // if there are no matching projects, then check against all bots, not just project bots
-        if (filteredProjects == null || filteredProjects.isEmpty()) {
-            return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
-        }
-        // for each of the matched projects, check the bot for the matching project ID
-        for (Project p : filteredProjects) {
-            LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
-            for (JsonNode bot : botObjs) {
-                // if the project ID match, and one of the email fields matches, then user is bot
-                if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
-                        && checkFieldsForMatchingMail(bot, mail)) {
-                    return true;
-                }
-            }
+  private boolean userIsABot(String mail, List<Project> filteredProjects) {
+    if (mail == null || "".equals(mail.trim())) {
+      return false;
+    }
+    List<JsonNode> botObjs = getBots();
+    // if there are no matching projects, then check against all bots, not just
+    // project bots
+    if (filteredProjects == null || filteredProjects.isEmpty()) {
+      return botObjs.stream().anyMatch(bot -> checkFieldsForMatchingMail(bot, mail));
+    }
+    // for each of the matched projects, check the bot for the matching project ID
+    for (Project p : filteredProjects) {
+      LOGGER.debug("Checking project {} for matching bots", p.getProjectId());
+      for (JsonNode bot : botObjs) {
+        // if the project ID match, and one of the email fields matches, then user is
+        // bot
+        if (p.getProjectId().equalsIgnoreCase(bot.get("projectId").asText())
+            && checkFieldsForMatchingMail(bot, mail)) {
+          return true;
         }
-        return false;
+      }
     }
+    return false;
+  }
 
-    /**
-     * Checks JSON node to look for email fields, both at the root, and nested email fields.
-     * 
-     * @param bot the bots JSON object representation
-     * @param mail the email to match against
-     * @return true if the bot has a matching email value, otherwise false
-     */
-    private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
-        // check the root email for the bot for match
-        JsonNode botmail = bot.get("email");
-        if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
-            LOGGER.debug("Found matching bot at root level for '{}'",mail);
-            return true;
-        }
-        Iterator<Entry<String, JsonNode>> i = bot.fields();
-        while (i.hasNext()) {
-            Entry<String, JsonNode> e = i.next();
-            // check that our field is an object with fields
-            JsonNode node = e.getValue();
-            if (node.isObject()) {
-                LOGGER.debug("Checking {} for bot email", e.getKey());
-                // if the mail matches (ignoring case) user is bot
-                JsonNode botAliasMail = node.get("email");
-                if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
-                    LOGGER.debug("Found match for bot email {}", mail);
-                    return true;
-                }
-            }
+  /**
+   * Checks JSON node to look for email fields, both at the root, and nested email
+   * fields.
+   * 
+   * @param bot  the bots JSON object representation
+   * @param mail the email to match against
+   * @return true if the bot has a matching email value, otherwise false
+   */
+  private boolean checkFieldsForMatchingMail(JsonNode bot, String mail) {
+    // check the root email for the bot for match
+    JsonNode botmail = bot.get("email");
+    if (mail != null && botmail != null && mail.equalsIgnoreCase(botmail.asText(""))) {
+      LOGGER.debug("Found matching bot at root level for '{}'", mail);
+      return true;
+    }
+    Iterator<Entry<String, JsonNode>> i = bot.fields();
+    while (i.hasNext()) {
+      Entry<String, JsonNode> e = i.next();
+      // check that our field is an object with fields
+      JsonNode node = e.getValue();
+      if (node.isObject()) {
+        LOGGER.debug("Checking {} for bot email", e.getKey());
+        // if the mail matches (ignoring case) user is bot
+        JsonNode botAliasMail = node.get("email");
+        if (mail != null && botAliasMail != null && mail.equalsIgnoreCase(botAliasMail.asText(""))) {
+          LOGGER.debug("Found match for bot email {}", mail);
+          return true;
         }
-        return false;
+      }
     }
+    return false;
+  }
+
   private boolean isAllowedUser(String mail) {
     return allowListUsers.indexOf(mail) != -1;
   }
 
   /**
-   * Retrieves projects valid for the current request, or an empty list if no data or matching
+   * Retrieves projects valid for the current request, or an empty list if no data
+   * or matching
    * project repos could be found.
    *
    * @param req the current request
-   * @return list of matching projects for the current request, or an empty list if none found.
+   * @return list of matching projects for the current request, or an empty list
+   *         if none found.
    */
   private List<Project> retrieveProjectsForRequest(ValidationRequest req) {
     String repoUrl = req.getRepoUrl().getPath();
@@ -449,20 +499,22 @@ public class ValidationResource {
   }
 
   /**
-   * Retrieves an Eclipse Account user object given the Git users email address (at minimum). This
-   * is facilitated using the Eclipse Foundation accounts API, along short lived in-memory caching
+   * Retrieves an Eclipse Account user object given the Git users email address
+   * (at minimum). This
+   * is facilitated using the Eclipse Foundation accounts API, along short lived
+   * in-memory caching
    * for performance and some protection against duplicate requests.
    *
    * @param user the user to retrieve Eclipse Account information for
-   * @return the Eclipse Account user information if found, or null if there was an error or no user
-   *     exists.
+   * @return the Eclipse Account user information if found, or null if there was
+   *         an error or no user
+   *         exists.
    */
   private EclipseUser getIdentifiedUser(GitUser user) {
     // get the Eclipse account for the user
     try {
       // use cache to avoid asking for the same user repeatedly on repeated requests
-      Optional<EclipseUser> foundUser =
-          cache.get("user|" + user.getMail(), () -> retrieveUser(user), EclipseUser.class);
+      Optional<EclipseUser> foundUser = cache.get("user|" + user.getMail(), new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(user));
       if (!foundUser.isPresent()) {
         LOGGER.warn("No users found for mail '{}'", user.getMail());
         return null;
@@ -480,7 +532,8 @@ public class ValidationResource {
   }
 
   /**
-   * Checks for standard and noreply email address matches for a Git user and converts to a
+   * Checks for standard and noreply email address matches for a Git user and
+   * converts to a
    * Eclipse Foundation account object.
    * 
    * @param user the user to attempt account retrieval for.
@@ -495,30 +548,34 @@ public class ValidationResource {
     // standard user check (returns best match)
     LOGGER.debug("Checking user with mail {}", user.getMail());
     try {
-      List<EclipseUser> users = accounts.getUsers("Bearer " + oauth.getToken(), null, null, user.getMail());
-      if (users != null) {
+      List<EclipseUser> users = accounts.getUsers(null, null, user.getMail());
+      if (users != null && !users.isEmpty()) {
         return users.get(0);
       }
-    } catch(WebApplicationException e) {
+    } catch (WebApplicationException e) {
       LOGGER.warn("Could not find user account with mail '{}'", user.getMail());
     }
     return null;
   }
 
   /**
-   * Checks git user for no-reply address, and attempts to ratify user through reverse lookup in API service.
-   * Currently, this service only recognizes Github no-reply addresses as they have a route to be mapped.
+   * Checks git user for no-reply address, and attempts to ratify user through
+   * reverse lookup in API service.
+   * Currently, this service only recognizes Github no-reply addresses as they
+   * have a route to be mapped.
    * 
    * @param user the Git user account to check for no-reply mail address
-   * @return the Eclipse user if email address is detected no reply and one can be mapped, otherwise null
+   * @return the Eclipse user if email address is detected no reply and one can be
+   *         mapped, otherwise null
    */
   private EclipseUser checkForNoReplyUser(GitUser user) {
     LOGGER.debug("Checking user with mail {} for no-reply", user.getMail());
     boolean isNoReply = patterns.stream().anyMatch(pattern -> pattern.matcher(user.getMail().trim()).find());
     if (isNoReply) {
-      // get the username/ID string before the first @ symbol. 
+      // get the username/ID string before the first @ symbol.
       String noReplyUser = user.getMail().substring(0, user.getMail().indexOf("@", 0));
-      // split based on +, if more than one part, use second (contains user), otherwise, use whole string
+      // split based on +, if more than one part, use second (contains user),
+      // otherwise, use whole string
       String[] nameParts = noReplyUser.split("[\\+]");
       String namePart;
       if (nameParts.length > 1 && nameParts[1] != null) {
@@ -527,18 +584,18 @@ public class ValidationResource {
         namePart = nameParts[0];
       }
       String uname = namePart.trim();
-      LOGGER.debug("User with mail {} detected as noreply account, checking services for username match on '{}'", 
-        user.getMail(), uname);
+      LOGGER.debug("User with mail {} detected as noreply account, checking services for username match on '{}'",
+          user.getMail(), uname);
 
       // check github for no-reply (only allowed noreply currently)
       if (user.getMail().endsWith("noreply.github.com")) {
         try {
           // check for Github no reply, return if set
-          EclipseUser eclipseUser = accounts.getUserByGithubUname("Bearer " + oauth.getToken(), uname);
+          EclipseUser eclipseUser = accounts.getUserByGithubUname(uname);
           if (eclipseUser != null) {
             return eclipseUser;
           }
-        } catch(WebApplicationException e) {
+        } catch (WebApplicationException e) {
           LOGGER.warn("No match for '{}' in Github", uname);
         }
       }
@@ -546,14 +603,12 @@ public class ValidationResource {
     return null;
   }
 
-  @SuppressWarnings("unchecked")
   private List<JsonNode> getBots() {
-      Optional<List<JsonNode>> allBots = cache.get("allBots", () -> bots.getBots(),
-              (Class<List<JsonNode>>) (Object) List.class);
-      if (!allBots.isPresent()) {
-          return Collections.emptyList();
-      }
-      return allBots.get();
+    Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,  () -> bots.getBots());
+    if (!allBots.isPresent()) {
+      return Collections.emptyList();
+    }
+    return allBots.get();
   }
 
   private void addMessage(ValidationResponse r, String message, String hash) {
@@ -574,7 +629,7 @@ public class ValidationResource {
   private void addError(ValidationResponse r, String message, String hash, APIStatusCode code) {
     LOGGER.error(message);
     // only add as strict error for tracked projects
-    if (r.isTrackedProject() || r.isStrictMode()) {
+    if (r.getTrackedProject() || r.getStrictMode()) {
       r.addError(hash, message, code);
     } else {
       r.addWarning(hash, message, code);
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/CachingService.java b/src/main/java/org/eclipsefoundation/git/eca/service/CachingService.java
deleted file mode 100644
index 9795719c..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/CachingService.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-/* Copyright (c) 2019 Eclipse Foundation and others.
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Public License 2.0
- * which is available at http://www.eclipse.org/legal/epl-v20.html,
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipsefoundation.git.eca.service;
-
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Callable;
-
-/**
- * Interface defining the caching service to be used within the application.
- * 
- * @author Martin Lowe
- * @param <T> the type of object to be stored in the cache.
- */
-public interface CachingService {
-
-	/**
-	 * Returns an Optional object of type T, returning a cached object if available,
-	 * otherwise using the callable to generate a value to be stored in the cache
-	 * and returned.
-	 * 
-	 * @param cacheKey the cache key of the object to store in the cache
-	 * @param callable a runnable that returns an object of type T
-	 * @return the cached result
-	 */
-	<T> Optional<T> get(String cacheKey, Callable<? extends T> callable, Class<T> clazz);
-
-	/**
-	 * Returns the expiration date in millis since epoch.
-	 * 
-	 * @param cacheKey the cache key to check for a value, and if set its
-	 *                 expiration.
-	 * @return an Optional expiration date for the current object if its set. If
-	 *         there is no underlying data, then empty would be returned
-	 */
-	Optional<Long> getExpiration(String cacheKey);
-
-	/**
-	 * @return the max age of cache entries
-	 */
-	long getMaxAge();
-
-	/**
-	 * Retrieves a set of cache keys available to the current cache.
-	 * 
-	 * @return unmodifiable set of cache entry keys.
-	 */
-	Set<String> getCacheKeys();
-
-	/**
-	 * Removes cache entry for given cache entry key.
-	 * 
-	 * @param key cache entry key
-	 */
-	void remove(String key);
-
-	/**
-	 * Removes all cache entries.
-	 */
-	void removeAll();
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
deleted file mode 100644
index 6b8b6a08..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.service;
-
-/**
- * Used to generate OAuth tokens for use with internal services rather than
- * bolted on introspection. This is required over the (now deprecated) Elytron
- * plugin or the OIDC plugin as those plugins work with requests to validate
- * incoming rather than outgoing requests.
- * 
- * @author Martin Lowe
- *
- */
-public interface OAuthService {
-
-	/**
-	 * Retrieve an access token for the service from the Eclipse API for internal
-	 * usage.
-	 * 
-	 * @return current access token, or null if none could be retrieved for current
-	 *         API credentials/settings.
-	 */
-	String getToken();
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
deleted file mode 100644
index a3e837f8..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.service.impl;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.PostConstruct;
-import javax.inject.Singleton;
-
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.git.eca.oauth.EclipseApi;
-import org.eclipsefoundation.git.eca.service.OAuthService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.github.scribejava.core.builder.ServiceBuilder;
-import com.github.scribejava.core.model.OAuth2AccessToken;
-import com.github.scribejava.core.oauth.OAuth20Service;
-
-/**
- * Default implementation for requesting an OAuth request token. The reason that
- * this class is implemented over the other implementations baked into Quarkus
- * 
- * @author Martin Lowe
- *
- */
-@Singleton
-public class DefaultOAuthService implements OAuthService {
-	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuthService.class);
-
-	@ConfigProperty(name = "oauth2.client-id")
-	String id;
-	@ConfigProperty(name = "oauth2.client-secret")
-	String secret;
-	@ConfigProperty(name = "oauth2.scope")
-	String scope;
-
-	// service reference (as we only need one)
-	private OAuth20Service service;
-
-	// token state vars
-	private long expirationTime;
-	private String accessToken;
-
-	/**
-	 * Create an OAuth service reference.
-	 */
-	@PostConstruct
-	void createServiceRef() {
-		this.service = new ServiceBuilder(id).apiSecret(secret).scope(scope).build(EclipseApi.instance());
-	}
-
-	@Override
-	public String getToken() {
-		// lock on the class instance to stop multiple threads from requesting new
-		// tokens at the same time
-		synchronized (this) {
-			if (accessToken == null || System.currentTimeMillis() >= expirationTime) {
-				// clear access token
-				this.accessToken = null;
-				try {
-					OAuth2AccessToken requestToken = service.getAccessTokenClientCredentialsGrant();
-					if (requestToken != null) {
-						this.accessToken = requestToken.getAccessToken();
-						this.expirationTime = System.currentTimeMillis()
-								+ TimeUnit.SECONDS.toMillis(requestToken.getExpiresIn().longValue());
-					}
-				} catch (IOException e) {
-					LOGGER.error("Issue communicating with OAuth server for authentication", e);
-				} catch (InterruptedException e) {
-					LOGGER.error("Authentication communication was interrupted before completion", e);
-					Thread.currentThread().interrupt();
-				} catch (ExecutionException e) {
-					LOGGER.error("Error while retrieving access token for request", e);
-				}
-			}
-		}
-		return accessToken;
-	}
-
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/GuavaCachingService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/GuavaCachingService.java
deleted file mode 100644
index 2dcf7b5c..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/GuavaCachingService.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-/* Copyright (c) 2019 Eclipse Foundation and others.
- * This program and the accompanying materials are made available
- * under the terms of the Eclipse Public License 2.0
- * which is available at http://www.eclipse.org/legal/epl-v20.html,
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.eclipsefoundation.git.eca.service.impl;
-
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.PostConstruct;
-import javax.enterprise.context.ApplicationScoped;
-
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.git.eca.service.CachingService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
-import com.google.common.util.concurrent.UncheckedExecutionException;
-
-/**
- * <p>
- * Simple caching service for caching objects in an in-memory cache, implemented
- * using the Google Guava cache mechanism. Cache size and time to live are
- * configured within the MicroProfile configuration.
- * </p>
- * 
- * <p>
- * Guava cache is inherently thread safe, so no synchronization needs to be done
- * on access.
- * </p>
- * 
- * @author Martin Lowe
- * @param <T> the type of object cached by this instance of the service
- *
- */
-@ApplicationScoped
-public class GuavaCachingService implements CachingService {
-	private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCachingService.class);
-
-	@ConfigProperty(name = "cache.max.size", defaultValue = "10000")
-	long maxSize;
-	@ConfigProperty(name = "cache.ttl.write.seconds", defaultValue = "900")
-	long ttlWrite;
-
-	// actual cache object
-	private Map<Class<?>, Cache<String, ?>> caches;
-	private Map<String, Long> ttl;
-
-	@PostConstruct
-	public void init() {
-		this.ttl = new HashMap<>();
-		this.caches = new HashMap<>();
-	}
-
-	@Override
-	public <T> Optional<T> get(String cacheKey, Callable<? extends T> callable, Class<T> clazz) {
-		Objects.requireNonNull(cacheKey);
-		Objects.requireNonNull(callable);
-		try {
-			// multi cache support
-			@SuppressWarnings("unchecked")
-			Cache<String, T> c = (Cache<String, T>) caches.computeIfAbsent(clazz, key -> createCache());
-			
-			// get entry, and enter a ttl as soon as it returns
-			T data = c.get(cacheKey, callable);
-			if (data != null) {
-				ttl.putIfAbsent(cacheKey,
-						System.currentTimeMillis() + TimeUnit.MILLISECONDS.convert(ttlWrite, TimeUnit.SECONDS));
-			}
-			return Optional.of(c.get(cacheKey, callable));
-		} catch (InvalidCacheLoadException | UncheckedExecutionException e) {
-			LOGGER.error("Error while retrieving fresh value for cachekey: {}", cacheKey, e);
-		} catch (Exception e) {
-			LOGGER.error("Error while retrieving value of callback", e);
-		}
-		return Optional.empty();
-	}
-	
-	private <T> Cache<String, T> createCache() {
-		return CacheBuilder
-			.newBuilder()
-			.maximumSize(maxSize)
-			.expireAfterWrite(ttlWrite, TimeUnit.SECONDS)
-			.removalListener(not -> ttl.remove(not.getKey()))
-			.build();
-	}
-
-	@Override
-	public Optional<Long> getExpiration(String cacheKey) {
-		return Optional.ofNullable(ttl.get(cacheKey));
-	}
-	
-	@Override
-	public Set<String> getCacheKeys() {
-		// create a set and return all keys
-		Set<String> out = new HashSet<>();
-		caches.values().stream().forEach(c -> out.addAll(c.asMap().keySet()));
-		return out;
-	}
-
-	@Override
-	public void remove(String key) {
-		// TODO we probably want to shift this to target a cache
-		caches.values().stream().forEach(c -> c.invalidate(key));
-	}
-
-	@Override
-	public void removeAll() {
-		// TODO we probably want to be able to target a given cache
-		caches.values().stream().forEach(Cache::invalidateAll);
-	}
-
-	@Override
-	public long getMaxAge() {
-		return ttlWrite;
-	}
-}
diff --git a/src/main/js/openapi2schema.js b/src/main/js/openapi2schema.js
new file mode 100644
index 00000000..e857cb33
--- /dev/null
+++ b/src/main/js/openapi2schema.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021 Eclipse
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * Author: Martin Lowe <martin.lowe@eclipsefoundation.org>
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+ const toJsonSchema = require('@openapi-contrib/openapi-schema-to-json-schema');
+ const Resolver = require('@stoplight/json-ref-resolver');
+ const yaml = require('js-yaml');
+ const fs = require('fs');
+ const decamelize = require('decamelize');
+ const args = require('yargs')
+   .option('s', {
+     alias: 'src',
+     desc: 'The fully qualified path to the YAML spec.',
+   })
+   .option('t', {
+     alias: 'target',
+     desc: 'The fully qualified path to write the JSON schema to',
+   }).argv;
+ if (!args.s || !args.t) {
+   process.exit(1);
+ }
+ 
+ run();
+ 
+ /**
+  * Generates JSON schema files for consumption of the Java tests.
+  */
+ async function run() {
+   try {
+     // load in the openapi yaml spec as an object
+     const doc = yaml.load(fs.readFileSync(args.s, 'utf8'));
+     // resolve $refs in openapi spec
+     let resolvedInp = await new Resolver.Resolver().resolve(doc);
+     const out = toJsonSchema(resolvedInp.result);
+     // if folder doesn't exist, create it
+     if (!fs.existsSync(`${args.t}/schemas`)) {
+       fs.mkdirSync(`${args.t}/schemas`);
+     }
+     // for each of the schemas, generate a JSON schema file
+     for (let schemaName in out.components.schemas) {
+       fs.writeFileSync(`${args.t}/schemas/${decamelize(schemaName, { separator: '-' })}-schema.json`, JSON.stringify(out.components.schemas[schemaName]));
+     }
+   } catch (e) {
+     console.log(e);
+   }
+ }
+ 
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource b/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
deleted file mode 100644
index acd2ee19..00000000
--- a/src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource
+++ /dev/null
@@ -1 +0,0 @@
-org.eclipsefoundation.git.eca.config.SecretConfigSource
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 94936fec..f41f5d75 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -8,11 +8,15 @@ eclipse.noreply.email-patterns=@users.noreply.github.com\$
 quarkus.http.root-path=/git
 
 ## OAUTH CONFIG
-oauth2.scope=eclipsefdn_view_all_profiles
 quarkus.http.port=8080
 
-## required to start when secret.properties isn't found/mounted
-oauth2.client-id=placeholder
-oauth2.client-secret=placeholder
+quarkus.keycloak.devservices.enabled=false
+quarkus.oauth2.enabled=false
+quarkus.oidc.enabled=false
+quarkus.oidc-client.auth-server-url=https://accounts.eclipse.org/oauth2
+quarkus.oidc-client.discovery-enabled=false
+quarkus.oidc-client.token-path=/token
+quarkus.oidc-client.grant.type=client
+quarkus.oidc-client.scopes=eclipsefdn_view_all_profiles
 
 eclipse.mail.allowlist=noreply@github.com
\ No newline at end of file
diff --git a/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java
index a9f53696..a96a9cd5 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java
@@ -27,83 +27,52 @@ import io.quarkus.test.Mock;
 @ApplicationScoped
 public class MockAccountsAPI implements AccountsAPI {
 
-	private List<EclipseUser> src;
-	
-	@PostConstruct
-	public void build() {
-		this.src = new ArrayList<>();
-		int id = 0;
+    private List<EclipseUser> src;
 
-		EclipseUser e1 = new EclipseUser();
-		e1.setCommitter(false);
-		e1.setId(id++);
-		e1.setMail("newbie@important.co");
-		e1.setName("newbieAnon");
-		e1.setEca(new ECA());
-		src.add(e1);
-		
-		EclipseUser e2 = new EclipseUser();
-		e2.setCommitter(false);
-		e2.setId(id++);
-		e2.setMail("slom@eclipse-foundation.org");
-		e2.setName("barshall_blathers");
-		e2.setEca(new ECA(true, true));
-		src.add(e2);
-		
-		EclipseUser e3 = new EclipseUser();
-		e3.setCommitter(false);
-		e3.setId(id++);
-		e3.setMail("tester@eclipse-foundation.org");
-		e3.setName("mctesterson");
-		e3.setEca(new ECA(true, false));
-		src.add(e3);
-		
-		EclipseUser e4 = new EclipseUser();
-		e4.setCommitter(true);
-		e4.setId(id++);
-		e4.setMail("code.wiz@important.co");
-		e4.setName("da_wizz");
-		e4.setEca(new ECA(true, true));
-		src.add(e4);
-		
-		EclipseUser e5 = new EclipseUser();
-		e5.setCommitter(true);
-		e5.setId(id++);
-		e5.setMail("grunt@important.co");
-		e5.setName("grunter");
-		e5.setEca(new ECA(true, false));
-		src.add(e5);
-		
-		EclipseUser e6 = new EclipseUser();
-		e6.setCommitter(false);
-		e6.setId(id++);
-		e6.setMail("paper.pusher@important.co");
-		e6.setName("sumAnalyst");
-		e6.setEca(new ECA(true, false));
-		src.add(e6);
-	}
-	
-	@Override
-	public List<EclipseUser> getUsers(String authBearer, String id, String name, String mail) {
-		return src.stream().filter(user -> {
-			boolean matches = true;
-			if (id != null && !Integer.toString(user.getId()).equals(id)) {
-				matches = false;
-			}
-			if (name != null && !user.getName().equals(name)) {
-				matches = false;
-			}
-			if (mail != null && !user.getMail().equalsIgnoreCase(mail)) {
-				matches = false;
-			}
-			return matches;
-		}).collect(Collectors.toList());
-	}
+    @PostConstruct
+    public void build() {
+        this.src = new ArrayList<>();
+        int id = 0;
+        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("newbie@important.co")
+                .setName("newbieAnon").setECA(ECA.builder().build()).build());
+        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("slom@eclipse-foundation.org")
+                .setName("barshall_blathers")
+                .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
+        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("tester@eclipse-foundation.org")
+                .setName("mctesterson").setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build())
+                .build());
+        src.add(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("code.wiz@important.co")
+                .setName("da_wizz").setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build())
+                .build());
+        src.add(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co").setName("grunter")
+                .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("paper.pusher@important.co")
+                .setName("sumAnalyst").setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build())
+                .build());
 
-	@Override
-	public EclipseUser getUserByGithubUname(String authBearer, String uname) {
-		// assume GH username == Eclipse uname for simplicity of test
-		return src.stream().filter(user -> uname.equalsIgnoreCase(user.getName())).findFirst().orElse(null);
-	}
+    }
+
+    @Override
+    public List<EclipseUser> getUsers(String id, String name, String mail) {
+        return src.stream().filter(user -> {
+            boolean matches = true;
+            if (id != null && !Integer.toString(user.getUid()).equals(id)) {
+                matches = false;
+            }
+            if (name != null && !user.getName().equals(name)) {
+                matches = false;
+            }
+            if (mail != null && !user.getMail().equalsIgnoreCase(mail)) {
+                matches = false;
+            }
+            return matches;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public EclipseUser getUserByGithubUname(String uname) {
+        // assume GH username == Eclipse uname for simplicity of test
+        return src.stream().filter(user -> uname.equalsIgnoreCase(user.getName())).findFirst().orElse(null);
+    }
 
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java
index 64d90666..e4c103b4 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java
@@ -12,7 +12,9 @@ package org.eclipsefoundation.git.eca.api;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
@@ -29,71 +31,56 @@ import io.quarkus.test.Mock;
 @ApplicationScoped
 public class MockProjectsAPI implements ProjectsAPI {
 
-	private List<Project> src;
+    private List<Project> src;
 
-	@PostConstruct
-	public void build() {
-		this.src = new ArrayList<>();
+    @PostConstruct
+    public void build() {
+        this.src = new ArrayList<>();
 
-		// sample repos
-		Repo r1 = new Repo();
-		r1.setUrl("http://www.github.com/eclipsefdn/sample");
-		Repo r2 = new Repo();
-		r2.setUrl("http://www.github.com/eclipsefdn/test");
-		Repo r3 = new Repo();
-		r3.setUrl("http://www.github.com/eclipsefdn/prototype.git");
-		Repo r4 = new Repo();
-		r4.setUrl("http://www.github.com/eclipsefdn/tck-proto");
-		Repo r5 = new Repo();
-		r5.setUrl("/gitroot/sample/gerrit.project.git");
-		Repo r6 = new Repo();
-		r6.setUrl("/gitroot/sample/gerrit.other-project");
-		Repo r7 = new Repo();
-		r7.setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.git");
-		Repo r8 = new Repo();
-		r8.setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test");
+        // sample repos
+        Repo r1 = new Repo();
+        r1.setUrl("http://www.github.com/eclipsefdn/sample");
+        Repo r2 = new Repo();
+        r2.setUrl("http://www.github.com/eclipsefdn/test");
+        Repo r3 = new Repo();
+        r3.setUrl("http://www.github.com/eclipsefdn/prototype.git");
+        Repo r4 = new Repo();
+        r4.setUrl("http://www.github.com/eclipsefdn/tck-proto");
+        Repo r5 = new Repo();
+        r5.setUrl("/gitroot/sample/gerrit.project.git");
+        Repo r6 = new Repo();
+        r6.setUrl("/gitroot/sample/gerrit.other-project");
+        Repo r7 = new Repo();
+        r7.setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.git");
+        Repo r8 = new Repo();
+        r8.setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test");
 
-		// sample users, correlates to users in Mock projects API
-		User u1 = new User();
-		u1.setUrl("");
-		u1.setUsername("da_wizz");
+        // sample users, correlates to users in Mock projects API
+        User u1 = User.builder().setUrl("").setUsername("da_wizz").build();
+        User u2 = User.builder().setUrl("").setUsername("grunter").build();
 
-		User u2 = new User();
-		u2.setUrl("");
-		u2.setUsername("grunter");
+        // projects
+        Project p1 = Project.builder().setName("Sample project").setProjectId("sample.proj")
+                .setSpecProjectWorkingGroup(Collections.emptyList()).setGithubRepos(Arrays.asList(r1, r2))
+                .setGerritRepos(Arrays.asList(r5)).setCommitters(Arrays.asList(u1, u2)).build();
+        src.add(p1);
 
-		// projects
-		Project p1 = new Project();
-		p1.setName("Sample project");
-		p1.setProjectId("sample.proj");
-		p1.setSpecWorkingGroup(null);
-		p1.setGithubRepos(Arrays.asList(r1, r2));
-		p1.setGerritRepos(Arrays.asList(r5));
-		p1.setCommitters(Arrays.asList(u1, u2));
-		src.add(p1);
+        Project p2 = Project.builder().setName("Prototype thing").setProjectId("sample.proto")
+                .setSpecProjectWorkingGroup(Collections.emptyList()).setGithubRepos(Arrays.asList(r3))
+                .setGerritRepos(Arrays.asList(r6)).setGitlabRepos(Arrays.asList(r8)).setCommitters(Arrays.asList(u2))
+                .build();
+        src.add(p2);
 
-		Project p2 = new Project();
-		p2.setName("Prototype thing");
-		p2.setProjectId("sample.proto");
-		p2.setSpecWorkingGroup(null);
-		p2.setGithubRepos(Arrays.asList(r3));
-		p2.setGerritRepos(Arrays.asList(r6));
-		p2.setGitlabRepos(Arrays.asList(r8));
-		p2.setCommitters(Arrays.asList(u2));
-		src.add(p2);
+        Map<String, String> map = new HashMap<>();
+        map.put("id", "proj1");
+        Project p3 = Project.builder().setName("Spec project").setProjectId("spec.proj").setSpecProjectWorkingGroup(map)
+                .setGithubRepos(Arrays.asList(r4)).setGitlabRepos(Arrays.asList(r7))
+                .setCommitters(Arrays.asList(u1, u2)).build();
+        src.add(p3);
+    }
 
-		Project p3 = new Project();
-		p3.setName("Spec project");
-		p3.setProjectId("spec.proj");
-		p3.setSpecWorkingGroup("proj1");
-		p3.setGithubRepos(Arrays.asList(r4));
-		p3.setGitlabRepos(Arrays.asList(r7));
-		p3.setCommitters(Arrays.asList(u1, u2));
-		src.add(p3);
-	}
-
-	@Override
-	public List<Project> getProject(int page, int pageSize) {
-		return page == 1 ? new ArrayList<>(src) : Collections.emptyList();
-	}
+    @Override
+    public List<Project> getProject(int page, int pageSize) {
+        return page == 1 ? new ArrayList<>(src) : Collections.emptyList();
+    }
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
index a4ce4684..15516403 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/helper/CommitHelperTest.java
@@ -28,98 +28,87 @@ import io.quarkus.test.junit.QuarkusTest;
 @QuarkusTest
 class CommitHelperTest {
 
-	// represents a known good commit before the start of each test
-	GitUser testUser;
-	Commit baseCommit;
-
-	@BeforeEach
-	void setup() {
-		// basic good user
-		testUser = new GitUser();
-		testUser.setMail("test.user@eclipse-foundation.org");
-		testUser.setName("Tester McTesterson");
-
-		// basic known good commit
-		baseCommit = new Commit();
-		baseCommit.setBody(
-				String.format("Sample body content\n\nSigned-off-by: %s <%s>", testUser.getName(), testUser.getMail()));
-		baseCommit.setHash("abc123f");
-		baseCommit.setHead(false);
-		baseCommit.setParents(new ArrayList<>());
-		baseCommit.setSubject("Testing CommitHelper class #1337");
-		baseCommit.setAuthor(testUser);
-		baseCommit.setCommitter(testUser);
-	}
-
-	@Test
-	void validateCommitKnownGood() {
-		Assertions.assertTrue(CommitHelper.validateCommit(baseCommit), "Expected basic commit to pass validation");
-	}
-	
-	@Test
-	void validateCommitNullCommit() {
-		Assertions.assertFalse(CommitHelper.validateCommit(null), "Expected null commit to fail validation");
-	}
-
-	@Test
-	void validateCommitNoAuthor() {
-		baseCommit.setAuthor(null);
-		Assertions.assertFalse(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to fail validation w/ no author");
-	}
-
-	@Test
-	void validateCommitNoAuthorMail() {
-		GitUser noMail = new GitUser();
-		noMail.setName("Some Name");
-
-		baseCommit.setAuthor(noMail);
-		Assertions.assertFalse(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to fail validation w/ no author mail address");
-	}
-
-	@Test
-	void validateCommitNoCommitter() {
-		baseCommit.setCommitter(null);
-		Assertions.assertFalse(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to fail validation w/ no committer");
-	}
-
-	@Test
-	void validateCommitNoCommitterMail() {
-		GitUser noMail = new GitUser();
-		noMail.setName("Some Name");
-
-		baseCommit.setCommitter(noMail);
-		Assertions.assertFalse(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to fail validation w/ no committer mail address");
-	}
-
-	@Test
-	void validateCommitNoHash() {
-		baseCommit.setHash(null);
-		Assertions.assertFalse(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to fail validation w/ no commit hash");
-	}
-
-	@Test
-	void validateCommitNoBody() {
-		baseCommit.setBody(null);
-		Assertions.assertTrue(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to pass validation w/ no body");
-	}
-
-	@Test
-	void validateCommitNoParents() {
-		baseCommit.setParents(new ArrayList<>());
-		Assertions.assertTrue(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to pass validation w/ no parents");
-	}
-
-	@Test
-	void validateCommitNoSubject() {
-		baseCommit.setSubject(null);
-		Assertions.assertTrue(CommitHelper.validateCommit(baseCommit),
-				"Expected basic commit to pass validation w/ no subject");
-	}
+    // represents a known good commit before the start of each test
+    GitUser testUser;
+    Commit.Builder baseCommit;
+
+    @BeforeEach
+    void setup() {
+        // basic good user
+        testUser = GitUser.builder().setMail("test.user@eclipse-foundation.org").setName("Tester McTesterson").build();
+
+        // basic known good commit
+        baseCommit = Commit.builder()
+                .setBody(String.format("Sample body content\n\nSigned-off-by: %s <%s>", testUser.getName(),
+                        testUser.getMail()))
+                .setHash("abc123f").setHead(false).setParents(new ArrayList<>())
+                .setSubject("Testing CommitHelper class #1337").setAuthor(testUser).setCommitter(testUser);
+    }
+
+    @Test
+    void validateCommitKnownGood() {
+        Assertions.assertTrue(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to pass validation");
+    }
+
+    @Test
+    void validateCommitNullCommit() {
+        Assertions.assertFalse(CommitHelper.validateCommit(null), "Expected null commit to fail validation");
+    }
+
+    @Test
+    void validateCommitNoAuthor() {
+        baseCommit.setAuthor(null);
+        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to fail validation w/ no author");
+    }
+
+    @Test
+    void validateCommitNoAuthorMail() {
+        baseCommit.setAuthor(GitUser.builder().setName("Some Name").build());
+        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to fail validation w/ no author mail address");
+    }
+
+    @Test
+    void validateCommitNoCommitter() {
+        baseCommit.setCommitter(null);
+        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to fail validation w/ no committer");
+    }
+
+    @Test
+    void validateCommitNoCommitterMail() {
+        baseCommit.setCommitter(GitUser.builder().setName("Some Name").build());
+        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to fail validation w/ no committer mail address");
+    }
+
+    @Test
+    void validateCommitNoHash() {
+        baseCommit.setHash(null);
+        Assertions.assertFalse(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to fail validation w/ no commit hash");
+    }
+
+    @Test
+    void validateCommitNoBody() {
+        baseCommit.setBody(null);
+        Assertions.assertTrue(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to pass validation w/ no body");
+    }
+
+    @Test
+    void validateCommitNoParents() {
+        baseCommit.setParents(new ArrayList<>());
+        Assertions.assertTrue(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to pass validation w/ no parents");
+    }
+
+    @Test
+    void validateCommitNoSubject() {
+        baseCommit.setSubject(null);
+        Assertions.assertTrue(CommitHelper.validateCommit(baseCommit.build()),
+                "Expected basic commit to pass validation w/ no subject");
+    }
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
index 0738f5ec..657ec486 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -20,22 +20,21 @@ import java.util.List;
 
 import javax.inject.Inject;
 
+import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
-import org.eclipsefoundation.git.eca.service.CachingService;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.http.ContentType;
-import io.restassured.response.ValidatableResponse;
 
 /**
- * Tests for verifying end to end validation via the endpoint. Uses restassured to create pseudo
- * requests, and Mock API endpoints to ensure that all data is kept internal for test checks.
+ * Tests for verifying end to end validation via the endpoint. Uses restassured to create pseudo requests, and Mock API
+ * endpoints to ensure that all data is kept internal for test checks.
  *
  * @author Martin Lowe
  */
@@ -44,1148 +43,765 @@ class ValidationResourceTest {
 
     @Inject
     CachingService cs;
-    
-    
+
     @BeforeEach
     void cacheClear() {
         // if dev servers are run on the same machine, some values may live in the cache
         cs.removeAll();
     }
-    
-  @Test
-  void validate() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("The Wizard");
-    g1.setMail("code.wiz@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody("Signed-off-by: The Wizard <code.wiz@important.co>");
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateMultipleCommits() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("The Wizard");
-    g1.setMail("code.wiz@important.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("Grunts McGee");
-    g2.setMail("grunt@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody("Signed-off-by: The Wizard <code.wiz@important.co>");
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    Commit c2 = new Commit();
-    c2.setAuthor(g2);
-    c2.setCommitter(g2);
-    c2.setBody("Signed-off-by: Grunts McGee<grunt@important.co>");
-    c2.setHash("c044dca1847c94e709601651339f88a5c82e3cc7");
-    c2.setSubject("Add in feature");
-    c2.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c2);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateMergeCommit() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Rando Calressian");
-    g1.setMail("rando@nowhere.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(
-        Arrays.asList(
-            "46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10",
-            "46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c11"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // No errors expected, should pass as only commit is a valid merge commit
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitNoSignOffCommitter() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Grunts McGee");
-    g1.setMail("grunt@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody("");
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    // Should be valid as Grunt is a committer on the prototype project
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitNoSignOffNonCommitter() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("The Wizard");
-    g1.setMail("code.wiz@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody("");
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype.git"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    // Should be valid as wizard has signed ECA
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitInvalidSignOff() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Barshall Blathers");
-    g1.setMail("slom@eclipse-foundation.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), "barshallb@personal.co"));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype.git"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    // Should be valid as signed off by footer is no longer checked
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitSignOffMultipleFooterLines_Last() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Barshall Blathers");
-    g1.setMail("slom@eclipse-foundation.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(
-        String.format(
-            "Change-Id: 0000000000000001\nSigned-off-by: %s <%s>", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitSignOffMultipleFooterLines_First() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Barshall Blathers");
-    g1.setMail("slom@eclipse-foundation.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(
-        String.format(
-            "Signed-off-by: %s <%s>\nChange-Id: 0000000000000001", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateCommitSignOffMultipleFooterLines_Multiple() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Barshall Blathers");
-    g1.setMail("slom@eclipse-foundation.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(
-        String.format(
-            "Change-Id: 0000000000000001\\nSigned-off-by: %s <%s>\nSigned-off-by: %s <%s>",
-            g1.getName(), g1.getMail(), g1.getName(), "barshallb@personal.co"));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-  }
-
-  @Test
-  void validateWorkingGroupSpecAccess() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("The Wizard");
-    g1.setMail("code.wiz@important.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("Grunts McGee");
-    g2.setMail("grunt@important.co");
-
-    // CASE 1: WG Spec project write access valid
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody("Signed-off-by: The Wizard <code.wiz@important.co>");
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Collections.emptyList());
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    // Should be valid as Wizard has spec project write access + is committer
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(200)
-        .body("passed", is(true), "errorCount", is(0));
-
-    // CASE 2: No WG Spec proj write access
-    commits = new ArrayList<>();
-    // create sample commits
-    c1 = new Commit();
-    c1.setAuthor(g2);
-    c1.setCommitter(g2);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto"));
-    vr.setCommits(commits);
-
-    // test output w/ assertions
-    // Should be invalid as Grunt does not have spec project write access
-    // Should have 2 errors, as both users get validated
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body(
-            "passed",
-            is(false),
-            "errorCount",
-            is(2),
-            "commits.123456789abcdefghijklmnop.errors[0].code",
-            is(APIStatusCode.ERROR_SPEC_PROJECT.getValue()));
-  }
-
-  @Test
-  void validateNoECA_author() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Newbie Anon");
-    g1.setMail("newbie@important.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("The Wizard");
-    g2.setMail("code.wiz@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g2);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Error should be singular + that there's no ECA on file
-    // Status 403 (forbidden) is the standard return for invalid requests
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body("passed", is(false), "errorCount", is(1));
-  }
-
-  @Test
-  void validateNoECA_committer() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Newbie Anon");
-    g1.setMail("newbie@important.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("The Wizard");
-    g2.setMail("code.wiz@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g2);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Error count should be 1 for just the committer access
-    // Status 403 (forbidden) is the standard return for invalid requests
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body("passed", is(false), "errorCount", is(1));
-  }
-  @Test
-  void validateNoECA_both() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Newbie Anon");
-    g1.setMail("newbie@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should have 2 errors, 1 for author entry and 1 for committer entry
-    // Status 403 (forbidden) is the standard return for invalid requests
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body("passed", is(false), "errorCount", is(2));
-  }
-
-  @Test
-  void validateAuthorNoEclipseAccount() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Rando Calressian");
-    g1.setMail("rando@nowhere.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("Grunts McGee");
-    g2.setMail("grunt@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g2);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Error should be singular + that there's no Eclipse Account on file for author
-    // Status 403 (forbidden) is the standard return for invalid requests
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body("passed", is(false), "errorCount", is(1));
-  }
-
-  @Test
-  void validateCommitterNoEclipseAccount() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Rando Calressian");
-    g1.setMail("rando@nowhere.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("Grunts McGee");
-    g2.setMail("grunt@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g2);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Error should be singular + that there's no Eclipse Account on file for committer
-    // Status 403 (forbidden) is the standard return for invalid requests
-    given()
-        .body(vr)
-        .contentType(ContentType.JSON)
-        .when()
-        .post("/eca")
-        .then()
-        .statusCode(403)
-        .body("passed", is(false), "errorCount", is(1));
-  }
-
-  @Test
-  void validateProxyCommitUntrackedProject() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("Rando Calressian");
-    g1.setMail("rando@nowhere.co");
-
-    GitUser g2 = new GitUser();
-    g2.setName("Grunts McGee");
-    g2.setMail("grunt@important.co");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g2);
-    c1.setCommitter(g1);
-    c1.setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()));
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-not-tracked"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be valid as project is not tracked
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGithub() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("projbot");
-    g1.setMail("1.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be valid as bots should only commit on their own projects (including aliases)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGithub_untracked() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("projbot");
-    g1.setMail("1.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-untracked"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be valid as bots can commit on any untracked project (legacy support)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGithub_invalidBot() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("protobot-gh");
-    g1.setMail("2.bot-github@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be invalid as bots should only commit on their own projects (including aliases)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateBotCommiterAccessGithub_wrongEmail() throws URISyntaxException {
-    // set up test users - uses Gerrit/LDAP email (wrong for case)
-    GitUser g1 = new GitUser();
-    g1.setName("protobot");
-    g1.setMail("2.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITHUB);
-    vr.setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be invalid as wrong email was used for bot (uses Gerrit bot email)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateBotCommiterAccessGitlab() throws URISyntaxException {
-      // set up test users
-      GitUser g1 = new GitUser();
-      g1.setName("protobot-gh");
-      g1.setMail("2.bot-github@eclipse.org");
-
-      List<Commit> commits = new ArrayList<>();
-      // create sample commits
-      Commit c1 = new Commit();
-      c1.setAuthor(g1);
-      c1.setCommitter(g1);
-      c1.setHash("123456789abcdefghijklmnop");
-      c1.setSubject("All of the things");
-      c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-      commits.add(c1);
-
-      ValidationRequest vr = new ValidationRequest();
-      vr.setProvider(ProviderType.GITLAB);
-      vr.setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test"));
-      vr.setCommits(commits);
-      // test output w/ assertions
-      // Should be valid as bots should only commit on their own projects (including aliases)
-      given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGitlab_untracked() throws URISyntaxException {
-      // set up test users
-      GitUser g1 = new GitUser();
-      g1.setName("protobot-gh");
-      g1.setMail("2.bot-github@eclipse.org");
-
-      List<Commit> commits = new ArrayList<>();
-      // create sample commits
-      Commit c1 = new Commit();
-      c1.setAuthor(g1);
-      c1.setCommitter(g1);
-      c1.setHash("123456789abcdefghijklmnop");
-      c1.setSubject("All of the things");
-      c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-      commits.add(c1);
-
-      ValidationRequest vr = new ValidationRequest();
-      vr.setProvider(ProviderType.GITLAB);
-      vr.setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.untracked"));
-      vr.setCommits(commits);
-      // test output w/ assertions
-      // Should be valid as bots can commit on any untracked project (legacy support)
-      given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-  
-  @Test
-  void validateBotCommiterAccessGitlab_invalidBot() throws URISyntaxException {
-    // set up test users (wrong bot for project)
-    GitUser g1 = new GitUser();
-    g1.setName("specbot");
-    g1.setMail("3.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITLAB);
-    vr.setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be invalid as bots should only commit on their own projects
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateBotCommiterAccessGitlab_wrongEmail() throws URISyntaxException {
-    // set up test users - uses Gerrit/LDAP email (expects Gitlab email)
-    GitUser g1 = new GitUser();
-    g1.setName("specbot");
-    g1.setMail("3.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GITLAB);
-    vr.setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.git"));
-    vr.setCommits(commits);
-    // test output w/ assertions
-    // Should be valid as wrong email was used, but is still bot email alias (uses Gerrit bot email)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGerrit() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("protobot");
-    g1.setMail("2.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as bots should only commit on their own projects (including aliases)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGerrit_untracked() throws URISyntaxException {
-    // set up test users
-    GitUser g1 = new GitUser();
-    g1.setName("protobot");
-    g1.setMail("2.bot@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/untracked.project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as bots can commit on any untracked project (legacy support)
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateBotCommiterAccessGerrit_invalidBot() throws URISyntaxException {
-      // set up test users -  (wrong bot for project)
-      GitUser g1 = new GitUser();
-      g1.setName("specbot");
-      g1.setMail("3.bot@eclipse.org");
-
-      List<Commit> commits = new ArrayList<>();
-      // create sample commits
-      Commit c1 = new Commit();
-      c1.setAuthor(g1);
-      c1.setCommitter(g1);
-      c1.setHash("123456789abcdefghijklmnop");
-      c1.setSubject("All of the things");
-      c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-      commits.add(c1);
-
-      ValidationRequest vr = new ValidationRequest();
-      vr.setProvider(ProviderType.GERRIT);
-      vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-      vr.setCommits(commits);
-      vr.setStrictMode(true);
-      // test output w/ assertions
-      // Should be invalid as bots should only commit on their own projects (wrong project)
-      given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-      
-  }
-
-  @Test
-  void validateBotCommiterAccessGerrit_aliasEmail() throws URISyntaxException {
-    // set up test users - uses GH (instead of expected Gerrit/LDAP email)
-    GitUser g1 = new GitUser();
-    g1.setName("protobot-gh");
-    g1.setMail("2.bot-github@eclipse.org");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as wrong email was used, but is still bot email alias 
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateNullEmailCheck() throws URISyntaxException {
-      // set up test users - uses GH (instead of expected Gerrit/LDAP email)
-      GitUser g1 = new GitUser();
-      g1.setName("protobot-gh");
-      g1.setMail("2.bot-github@eclipse.org");
-      GitUser g2 = new GitUser();
-      g2.setName("protobot-gh");
-
-      List<Commit> commits = new ArrayList<>();
-      // create sample commits
-      Commit c1 = new Commit();
-      c1.setAuthor(g1);
-      c1.setCommitter(g2);
-      c1.setHash("123456789abcdefghijklmnop");
-      c1.setSubject("All of the things");
-      c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-      commits.add(c1);
-
-      ValidationRequest vr = new ValidationRequest();
-      vr.setProvider(ProviderType.GERRIT);
-      vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-      vr.setCommits(commits);
-      vr.setStrictMode(true);
-      // test output w/ assertions
-      // Should be invalid as there is no email (refuse commit, not server error)
-      given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateGithubNoReply_legacy() throws URISyntaxException {
-    GitUser g1 = new GitUser();
-    g1.setName("grunter");
-    g1.setMail("grunter@users.noreply.github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateGithubNoReply_success() throws URISyntaxException {
-    // sometimes the user ID and user name are reversed
-    GitUser g1 = new GitUser();
-    g1.setName("grunter");
-    g1.setMail("123456789+grunter@users.noreply.github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateGithubNoReply_nomatch() throws URISyntaxException {
-    GitUser g1 = new GitUser();
-    g1.setName("some_guy");
-    g1.setMail("123456789+some_guy@users.noreply.github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be invalid as no user exists with "Github" handle that matches some_guy
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateGithubNoReply_nomatch_legacy() throws URISyntaxException {
-    GitUser g1 = new GitUser();
-    g1.setName("some_guy");
-    g1.setMail("some_guy@users.noreply.github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be invalid as no user exists with "Github" handle that matches some_guy
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
-  }
-
-  @Test
-  void validateAllowListAuthor_success() throws URISyntaxException {
-    GitUser g1 = new GitUser();
-    g1.setName("grunter");
-    g1.setMail("grunter@users.noreply.github.com");
-    GitUser g2 = new GitUser();
-    g2.setName("grunter");
-    g2.setMail("noreply@github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g2);
-    c1.setCommitter(g1);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
-
-  @Test
-  void validateAllowListCommitter_success() throws URISyntaxException {
-    GitUser g1 = new GitUser();
-    g1.setName("grunter");
-    g1.setMail("grunter@users.noreply.github.com");
-    GitUser g2 = new GitUser();
-    g2.setName("grunter");
-    g2.setMail("noreply@github.com");
-
-    List<Commit> commits = new ArrayList<>();
-    // create sample commits
-    Commit c1 = new Commit();
-    c1.setAuthor(g1);
-    c1.setCommitter(g2);
-    c1.setHash("123456789abcdefghijklmnop");
-    c1.setSubject("All of the things");
-    c1.setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"));
-    commits.add(c1);
-
-    ValidationRequest vr = new ValidationRequest();
-    vr.setProvider(ProviderType.GERRIT);
-    vr.setRepoUrl(new URI("/gitroot/sample/gerrit.other-project"));
-    vr.setCommits(commits);
-    vr.setStrictMode(true);
-    // test output w/ assertions
-    // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-    given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
-  }
+
+    @Test
+    void validate() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateMultipleCommits() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        GitUser g2 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        Commit c2 = Commit.builder().setAuthor(g2).setCommitter(g2)
+                .setBody("Signed-off-by: Grunts McGee<grunt@important.co>")
+                .setHash("c044dca1847c94e709601651339f88a5c82e3cc7").setSubject("Add in feature")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c2);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateMergeCommit() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Rando Calressian").setMail("rando@nowhere.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things").setParents(Arrays
+                        .asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10", "46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c11"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // No errors expected, should pass as only commit is a valid merge commit
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitNoSignOffCommitter() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // Should be valid as Grunt is a committer on the prototype project
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitNoSignOffNonCommitter() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setBody("").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype.git")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // Should be valid as wizard has signed ECA
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitInvalidSignOff() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Barshall Blathers").setMail("slom@eclipse-foundation.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), "barshallb@personal.co"))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype.git")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // Should be valid as signed off by footer is no longer checked
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitSignOffMultipleFooterLines_Last() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Barshall Blathers").setMail("slom@eclipse-foundation.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Change-Id: 0000000000000001\nSigned-off-by: %s <%s>", g1.getName(),
+                        g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitSignOffMultipleFooterLines_First() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Barshall Blathers").setMail("slom@eclipse-foundation.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>\nChange-Id: 0000000000000001", g1.getName(),
+                        g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateCommitSignOffMultipleFooterLines_Multiple() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Barshall Blathers").setMail("slom@eclipse-foundation.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Change-Id: 0000000000000001\\nSigned-off-by: %s <%s>\nSigned-off-by: %s <%s>",
+                        g1.getName(), g1.getMail(), g1.getName(), "barshallb@personal.co"))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
+
+        // test output w/ assertions
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+    }
+
+    @Test
+    void validateWorkingGroupSpecAccess() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        GitUser g2 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        // CASE 1: WG Spec project write access valid
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // Should be valid as Wizard has spec project write access + is committer
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+                is(true), "errorCount", is(0));
+
+        // CASE 2: No WG Spec proj write access
+        commits = new ArrayList<>();
+        // create sample commits
+        c1 = Commit.builder().setAuthor(g2).setCommitter(g2)
+                .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+        vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/tck-proto")).setCommits(commits).build();
+
+        // test output w/ assertions
+        // Should be invalid as Grunt does not have spec project write access
+        // Should have 2 errors, as both users get validated
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(2), "commits.123456789abcdefghijklmnop.errors[0].code",
+                is(APIStatusCode.ERROR_SPEC_PROJECT.getValue()));
+    }
+
+    @Test
+    void validateNoECA_author() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+
+        GitUser g2 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Error should be singular + that there's no ECA on file
+        // Status 403 (forbidden) is the standard return for invalid requests
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+    }
+
+    @Test
+    void validateNoECA_committer() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+
+        GitUser g2 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Error count should be 1 for just the committer access
+        // Status 403 (forbidden) is the standard return for invalid requests
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+    }
+
+    @Test
+    void validateNoECA_both() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should have 2 errors, 1 for author entry and 1 for committer entry
+        // Status 403 (forbidden) is the standard return for invalid requests
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(2));
+    }
+
+    @Test
+    void validateAuthorNoEclipseAccount() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Rando Calressian").setMail("rando@nowhere.co").build();
+
+        GitUser g2 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2)
+                .setBody(String.format("Signed-off-by: %s <%s>", g1.getName(), g1.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Error should be singular + that there's no Eclipse Account on file for author
+        // Status 403 (forbidden) is the standard return for invalid requests
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+    }
+
+    @Test
+    void validateCommitterNoEclipseAccount() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Rando Calressian").setMail("rando@nowhere.co").build();
+
+        GitUser g2 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Error should be singular + that there's no Eclipse Account on file for committer
+        // Status 403 (forbidden) is the standard return for invalid requests
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+                is(false), "errorCount", is(1));
+    }
+
+    @Test
+    void validateProxyCommitUntrackedProject() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("Rando Calressian").setMail("rando@nowhere.co").build();
+
+        GitUser g2 = GitUser.builder().setName("Grunts McGee").setMail("grunt@important.co").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1)
+                .setBody(String.format("Signed-off-by: %s <%s>", g2.getName(), g2.getMail()))
+                .setHash("123456789abcdefghijklmnop").setSubject("All of the things")
+                .setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10")).build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-not-tracked")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be valid as project is not tracked
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGithub() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("projbot").setMail("1.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be valid as bots should only commit on their own projects (including aliases)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGithub_untracked() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("projbot").setMail("1.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-untracked")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be valid as bots can commit on any untracked project (legacy support)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGithub_invalidBot() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("protobot-gh").setMail("2.bot-github@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be invalid as bots should only commit on their own projects (including aliases)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateBotCommiterAccessGithub_wrongEmail() throws URISyntaxException {
+        // set up test users - uses Gerrit/LDAP email (wrong for case)
+        GitUser g1 = GitUser.builder().setName("protobot").setMail("2.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be invalid as wrong email was used for bot (uses Gerrit bot email)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateBotCommiterAccessGitlab() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("protobot-gh").setMail("2.bot-github@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITLAB)
+                .setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test")).setCommits(commits)
+                .build();
+        // test output w/ assertions
+        // Should be valid as bots should only commit on their own projects (including aliases)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGitlab_untracked() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("protobot-gh").setMail("2.bot-github@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITLAB)
+                .setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.untracked"))
+                .setCommits(commits).build();
+        // test output w/ assertions
+        // Should be valid as bots can commit on any untracked project (legacy support)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGitlab_invalidBot() throws URISyntaxException {
+        // set up test users (wrong bot for project)
+        GitUser g1 = GitUser.builder().setName("specbot").setMail("3.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITLAB)
+                .setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.handbook.test")).setCommits(commits)
+                .build();
+        // test output w/ assertions
+        // Should be invalid as bots should only commit on their own projects
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateBotCommiterAccessGitlab_wrongEmail() throws URISyntaxException {
+        // set up test users - uses Gerrit/LDAP email (expects Gitlab email)
+        GitUser g1 = GitUser.builder().setName("specbot").setMail("3.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITLAB)
+                .setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.git")).setCommits(commits).build();
+        // test output w/ assertions
+        // Should be valid as wrong email was used, but is still bot email alias (uses Gerrit bot email)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGerrit() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("protobot").setMail("2.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as bots should only commit on their own projects (including aliases)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGerrit_untracked() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("protobot").setMail("2.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/untracked.project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as bots can commit on any untracked project (legacy support)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateBotCommiterAccessGerrit_invalidBot() throws URISyntaxException {
+        // set up test users - (wrong bot for project)
+        GitUser g1 = GitUser.builder().setName("specbot").setMail("3.bot@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be invalid as bots should only commit on their own projects (wrong project)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+
+    }
+
+    @Test
+    void validateBotCommiterAccessGerrit_aliasEmail() throws URISyntaxException {
+        // set up test users - uses GH (instead of expected Gerrit/LDAP email)
+        GitUser g1 = GitUser.builder().setName("protobot-gh").setMail("2.bot-github@eclipse.org").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as wrong email was used, but is still bot email alias
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateNullEmailCheck() throws URISyntaxException {
+        // set up test users - uses GH (instead of expected Gerrit/LDAP email)
+        GitUser g1 = GitUser.builder().setName("protobot-gh").setMail("2.bot-github@eclipse.org").build();
+        GitUser g2 = GitUser.builder().setName("protobot-gh").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be invalid as there is no email (refuse commit, not server error)
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateGithubNoReply_legacy() throws URISyntaxException {
+        GitUser g1 = GitUser.builder().setName("grunter").setMail("grunter@users.noreply.github.com").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as grunter used a no-reply Github account and has a matching GH handle
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateGithubNoReply_success() throws URISyntaxException {
+        // sometimes the user ID and user name are reversed
+        GitUser g1 = GitUser.builder().setName("grunter").setMail("123456789+grunter@users.noreply.github.com").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as grunter used a no-reply Github account and has a matching GH handle
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateGithubNoReply_nomatch() throws URISyntaxException {
+        GitUser g1 = GitUser.builder().setName("some_guy").setMail("123456789+some_guy@users.noreply.github.com")
+                .build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be invalid as no user exists with "Github" handle that matches some_guy
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateGithubNoReply_nomatch_legacy() throws URISyntaxException {
+        GitUser g1 = GitUser.builder().setName("some_guy").setMail("some_guy@users.noreply.github.com").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be invalid as no user exists with "Github" handle that matches some_guy
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+    }
+
+    @Test
+    void validateAllowListAuthor_success() throws URISyntaxException {
+        GitUser g1 = GitUser.builder().setName("grunter").setMail("grunter@users.noreply.github.com").build();
+        GitUser g2 = GitUser.builder().setName("grunter").setMail("noreply@github.com").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g2).setCommitter(g1).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as grunter used a no-reply Github account and has a matching GH handle
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
+
+    @Test
+    void validateAllowListCommitter_success() throws URISyntaxException {
+        GitUser g1 = GitUser.builder().setName("grunter").setMail("grunter@users.noreply.github.com").build();
+        GitUser g2 = GitUser.builder().setName("grunter").setMail("noreply@github.com").build();
+
+        List<Commit> commits = new ArrayList<>();
+        // create sample commits
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g2).setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Arrays.asList("46bb69bf6aa4ed26b2bf8c322ae05bef0bcc5c10"))
+                .build();
+        commits.add(c1);
+
+        ValidationRequest vr = ValidationRequest.builder().setProvider(ProviderType.GERRIT)
+                .setRepoUrl(new URI("/gitroot/sample/gerrit.other-project")).setCommits(commits).setStrictMode(true)
+                .build();
+        // test output w/ assertions
+        // Should be valid as grunter used a no-reply Github account and has a matching GH handle
+        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+    }
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/MockOAuthService.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/MockOAuthService.java
deleted file mode 100644
index 9d86b7c4..00000000
--- a/src/test/java/org/eclipsefoundation/git/eca/service/impl/MockOAuthService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.eclipsefoundation.git.eca.service.impl;
-
-import org.eclipsefoundation.git.eca.service.OAuthService;
-
-import io.quarkus.test.Mock;
-
-/**
- * Disable the OAuth service while in testing via a mock service. This will
- * never authenticate, but since all external data is mocked, this does not
- * impact testing.
- * 
- * @author Martin Lowe
- *
- */
-@Mock
-public class MockOAuthService implements OAuthService {
-
-	@Override
-	public String getToken() {
-		// return an empty (invalid) token every time
-		return "";
-	}
-
-}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 94936fec..e1cee2f0 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -7,12 +7,12 @@ eclipse.noreply.email-patterns=@users.noreply.github.com\$
 ## Expect to be mounted to '/git' to match current URL spec
 quarkus.http.root-path=/git
 
-## OAUTH CONFIG
-oauth2.scope=eclipsefdn_view_all_profiles
+## OIDC Connection/Authentication Info
+quarkus.oauth2.enabled=false
+quarkus.oidc.enabled=false
+quarkus.keycloak.devservices.enabled=false
+quarkus.oidc-client.enabled=false
 quarkus.http.port=8080
 
-## required to start when secret.properties isn't found/mounted
-oauth2.client-id=placeholder
-oauth2.client-secret=placeholder
-
-eclipse.mail.allowlist=noreply@github.com
\ No newline at end of file
+eclipse.mail.allowlist=noreply@github.com
+#quarkus.log.level=DEBUG
\ No newline at end of file
-- 
GitLab


From ec8965ca967cf01efcdf48a1d4b7ae1250688c58 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 27 Jan 2022 13:33:13 -0500
Subject: [PATCH 03/11] Update tests to ensure JSON format, some cleanup of
 package names

---
 .gitignore                                    |   5 +-
 spec/openapi.yaml                             | 155 ++++++++++++++++++
 .../eca/resource/ValidationResourceTest.java  | 122 ++++++++++----
 .../eca/{ => test}/api/MockAccountsAPI.java   |   3 +-
 .../git/eca/{ => test}/api/MockBotsAPI.java   |   3 +-
 .../eca/{ => test}/api/MockProjectsAPI.java   |   3 +-
 .../namespaces/SchemaNamespaceHelper.java     |  10 ++
 7 files changed, 261 insertions(+), 40 deletions(-)
 create mode 100644 spec/openapi.yaml
 rename src/test/java/org/eclipsefoundation/git/eca/{ => test}/api/MockAccountsAPI.java (97%)
 rename src/test/java/org/eclipsefoundation/git/eca/{ => test}/api/MockBotsAPI.java (96%)
 rename src/test/java/org/eclipsefoundation/git/eca/{ => test}/api/MockProjectsAPI.java (97%)
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java

diff --git a/.gitignore b/.gitignore
index 6604264c..03d0e35a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,4 +40,7 @@ release.properties
 # Secret store
 secret/
 secrets/
-secret.properties
\ No newline at end of file
+secret.properties
+
+# Additional build resources
+src/test/resources/schemas
\ No newline at end of file
diff --git a/spec/openapi.yaml b/spec/openapi.yaml
new file mode 100644
index 00000000..6fb0172c
--- /dev/null
+++ b/spec/openapi.yaml
@@ -0,0 +1,155 @@
+openapi: '3.1.0'
+info:
+   version: 1.1.0
+   title: Eclipse Foundation Git ECA API
+   license:
+      name: Eclipse Public License - 2.0
+      url: https://www.eclipse.org/legal/epl-2.0/
+servers:
+-  url: https://api.eclipse.org/git
+   description: Production endpoint for the Git ECA validation API
+tags:
+-  name: ECA Validation
+   description: Definitions in relation to the validation of Git commits through ECA signage
+paths:
+   /eca:
+      post:
+         tags:
+         - ECA Validation
+         summary: ECA validation
+         description: Validates a list of commits for a merge request.
+         requestBody:
+          content:
+            application/json:
+               schema: 
+                  $ref: '#/components/schemas/ValidationRequest'
+         responses:
+            200:
+               description: Success
+               content:
+                  application/json:
+                     schema:
+                        $ref: '#/components/schemas/ValidationResponse'
+            500:
+               description: Error while retrieving data
+components:
+   schemas:
+      NullableString:
+         description: A nullable String type value
+         type:
+            - 'null'
+            - string
+      DateTime:
+         type: string
+         format: datetime
+         description: |
+            Date string in the RFC 3339 format. Example, `1990-12-31T15:59:60-08:00`.
+
+            More on this standard can be read at https://tools.ietf.org/html/rfc3339.
+      ValidationRequest:
+        type: object
+        properties:
+          provider:
+            type: string
+            description: The provider for which the commit is being validated for
+            enum:
+              - github
+              - gitlab
+              - gerrit
+          repoUrl:
+            type: string
+            description: the outward facing URL of the repo the commit belongs to.
+          strictMode:
+            type: boolean
+            description: asd
+          commits:
+            type: array
+            minimum: 1
+            items:
+              type: object
+              properties:
+                hash:
+                  type: string
+                  description: The hash of the commit. Used for messaging and logging.
+                body:
+                  $ref: '#/components/schemas/NullableString'
+                  description: The body message of the commit if available
+                subject:
+                  $ref: '#/components/schemas/NullableString'
+                  description: The subject of the commit
+                author:
+                  $ref: '#/components/schemas/GitUser'
+                  description: The author of the Git commit
+                committer:
+                  $ref: '#/components/schemas/GitUser'
+                  description: The committer of the Git commit
+                head:
+                  type: 
+                    - boolean
+                    - 'null'
+                  description: True if the current commit is the head commit, false otherwise
+                parents:
+                  type: array
+                  items:
+                    type: string
+                    description: Parent commit hashes, multiple will be present for merge commits
+      GitUser:
+        type: object
+        properties:
+          name:
+            type: string
+            description: The name of the git user
+          mail:
+            type: string
+            description: the email address of the user
+      ValidationResponse:
+         type: object
+         properties:
+            time:
+               $ref: '#/components/schemas/DateTime'
+               description: Time of the request validation for logging/tracking purposes.
+            trackedProject:
+               type: boolean
+               description: Whether the project is tracked in PMI and is an Eclipse project.
+            strictMode:
+               type: boolean
+               description: Whether strict mode was enforced for the validation.
+            errorCount:
+               type: integer
+               description: The number of errors encountered while validating the request
+            passed:
+               type: boolean
+               description: Whether the current request is valid in relation to ECA signage.
+            commits:
+              type: object
+              propertyNames:
+                description: The commit hash of the commit that was validated
+              additionalProperties:
+                $ref: '#/components/schemas/Commit'
+      Commit:
+        type: object
+        properties:
+          messages:
+            type: array
+            description: List of informational messages about the validation of the current commit.
+            items:
+              $ref: '#/components/schemas/CommitMessage'
+          warnings:
+            type: array
+            description: List of non-fatal issues encountered in the validation of the current commit
+            items:
+              $ref: '#/components/schemas/CommitMessage'
+          errors:
+            type: array
+            description: List of errors encountered in the validation of the current commit
+            items:
+              $ref: '#/components/schemas/CommitMessage'
+      CommitMessage:
+        type: object
+        properties:
+          code:
+            type: integer
+            description: the internal status code for the message
+          message:
+            type: string
+            description: Information about the commit message. This can either be information about the validation process to report or the source of an error to be corrected.
diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
index 657ec486..886d1f6e 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -9,6 +9,7 @@
 package org.eclipsefoundation.git.eca.resource;
 
 import static io.restassured.RestAssured.given;
+import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
 import static org.hamcrest.CoreMatchers.is;
 
 import java.net.URI;
@@ -26,9 +27,14 @@ import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
+import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper;
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.http.ContentType;
 
@@ -40,9 +46,12 @@ import io.restassured.http.ContentType;
  */
 @QuarkusTest
 class ValidationResourceTest {
+    public static final String ECA_BASE_URL = "/eca";
 
     @Inject
     CachingService cs;
+    @Inject
+    ObjectMapper json;
 
     @BeforeEach
     void cacheClear() {
@@ -66,10 +75,51 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
 
         // test output w/ assertions
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
+    @Test
+    void validate_success_format() throws URISyntaxException {
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(Arrays.asList(c1)).build();
+        given().when().body(vr).contentType(ContentType.JSON).post(ECA_BASE_URL).then().assertThat()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.VALIDATION_RESPONSE_SCHEMA_PATH));
+    }
+
+    @Test
+    void validate_success_inputFormat() throws URISyntaxException {
+        // Check that the input matches what is specified in spec
+        // set up test users
+        GitUser g1 = GitUser.builder().setName("The Wizard").setMail("code.wiz@important.co").build();
+        Commit c1 = Commit.builder().setAuthor(g1).setCommitter(g1)
+                .setBody("Signed-off-by: The Wizard <code.wiz@important.co>").setHash("123456789abcdefghijklmnop")
+                .setSubject("All of the things").setParents(Collections.emptyList()).build();
+
+        ValidationRequest vr = ValidationRequest.builder().setStrictMode(false).setProvider(ProviderType.GITHUB)
+                .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(Arrays.asList(c1)).build();
+        // convert the object to JSON
+        String in;
+        try {
+            in = json.writeValueAsString(vr);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
+        System.out.println(in);
+        Assertions.assertTrue(
+                matchesJsonSchemaInClasspath(SchemaNamespaceHelper.VALIDATION_REQUEST_SCHEMA_PATH).matches(in));
+        // known good request
+        given().contentType(ContentType.JSON).body(vr).when().post(ECA_BASE_URL).then()
+                .body(matchesJsonSchemaInClasspath(SchemaNamespaceHelper.VALIDATION_RESPONSE_SCHEMA_PATH))
+                .statusCode(200);
+    }
+
     @Test
     void validateMultipleCommits() throws URISyntaxException {
         // set up test users
@@ -94,7 +144,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
 
         // test output w/ assertions
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -116,7 +166,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
         // test output w/ assertions
         // No errors expected, should pass as only commit is a valid merge commit
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -136,7 +186,7 @@ class ValidationResourceTest {
 
         // test output w/ assertions
         // Should be valid as Grunt is a committer on the prototype project
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -156,7 +206,7 @@ class ValidationResourceTest {
 
         // test output w/ assertions
         // Should be valid as wizard has signed ECA
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -178,7 +228,7 @@ class ValidationResourceTest {
 
         // test output w/ assertions
         // Should be valid as signed off by footer is no longer checked
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -200,7 +250,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
 
         // test output w/ assertions
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -222,7 +272,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
 
         // test output w/ assertions
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -244,7 +294,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/prototype")).setCommits(commits).build();
 
         // test output w/ assertions
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
     }
 
@@ -268,7 +318,7 @@ class ValidationResourceTest {
 
         // test output w/ assertions
         // Should be valid as Wizard has spec project write access + is committer
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200).body("passed",
                 is(true), "errorCount", is(0));
 
         // CASE 2: No WG Spec proj write access
@@ -285,7 +335,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Should be invalid as Grunt does not have spec project write access
         // Should have 2 errors, as both users get validated
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(2), "commits.123456789abcdefghijklmnop.errors[0].code",
                 is(APIStatusCode.ERROR_SPEC_PROJECT.getValue()));
     }
@@ -310,7 +360,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Error should be singular + that there's no ECA on file
         // Status 403 (forbidden) is the standard return for invalid requests
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(1));
     }
 
@@ -334,7 +384,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Error count should be 1 for just the committer access
         // Status 403 (forbidden) is the standard return for invalid requests
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(1));
     }
 
@@ -356,7 +406,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Should have 2 errors, 1 for author entry and 1 for committer entry
         // Status 403 (forbidden) is the standard return for invalid requests
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(2));
     }
 
@@ -380,7 +430,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Error should be singular + that there's no Eclipse Account on file for author
         // Status 403 (forbidden) is the standard return for invalid requests
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(1));
     }
 
@@ -404,7 +454,7 @@ class ValidationResourceTest {
         // test output w/ assertions
         // Error should be singular + that there's no Eclipse Account on file for committer
         // Status 403 (forbidden) is the standard return for invalid requests
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403).body("passed",
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403).body("passed",
                 is(false), "errorCount", is(1));
     }
 
@@ -427,7 +477,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-not-tracked")).setCommits(commits).build();
         // test output w/ assertions
         // Should be valid as project is not tracked
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -446,7 +496,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
         // test output w/ assertions
         // Should be valid as bots should only commit on their own projects (including aliases)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -465,7 +515,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample-untracked")).setCommits(commits).build();
         // test output w/ assertions
         // Should be valid as bots can commit on any untracked project (legacy support)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -484,7 +534,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
         // test output w/ assertions
         // Should be invalid as bots should only commit on their own projects (including aliases)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -503,7 +553,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("http://www.github.com/eclipsefdn/sample")).setCommits(commits).build();
         // test output w/ assertions
         // Should be invalid as wrong email was used for bot (uses Gerrit bot email)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -523,7 +573,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as bots should only commit on their own projects (including aliases)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -543,7 +593,7 @@ class ValidationResourceTest {
                 .setCommits(commits).build();
         // test output w/ assertions
         // Should be valid as bots can commit on any untracked project (legacy support)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -563,7 +613,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be invalid as bots should only commit on their own projects
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -582,7 +632,7 @@ class ValidationResourceTest {
                 .setRepoUrl(new URI("https://gitlab.eclipse.org/eclipse/dash/dash.git")).setCommits(commits).build();
         // test output w/ assertions
         // Should be valid as wrong email was used, but is still bot email alias (uses Gerrit bot email)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -602,7 +652,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as bots should only commit on their own projects (including aliases)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -622,7 +672,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as bots can commit on any untracked project (legacy support)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -642,7 +692,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be invalid as bots should only commit on their own projects (wrong project)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
 
     }
 
@@ -663,7 +713,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as wrong email was used, but is still bot email alias
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -684,7 +734,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be invalid as there is no email (refuse commit, not server error)
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -703,7 +753,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -723,7 +773,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -743,7 +793,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be invalid as no user exists with "Github" handle that matches some_guy
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -762,7 +812,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be invalid as no user exists with "Github" handle that matches some_guy
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(403);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(403);
     }
 
     @Test
@@ -782,7 +832,7 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 
     @Test
@@ -802,6 +852,6 @@ class ValidationResourceTest {
                 .build();
         // test output w/ assertions
         // Should be valid as grunter used a no-reply Github account and has a matching GH handle
-        given().body(vr).contentType(ContentType.JSON).when().post("/eca").then().statusCode(200);
+        given().body(vr).contentType(ContentType.JSON).when().post(ECA_BASE_URL).then().statusCode(200);
     }
 }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
similarity index 97%
rename from src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java
rename to src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
index a96a9cd5..1a93328c 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/api/MockAccountsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
@@ -7,7 +7,7 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
  ******************************************************************************/
-package org.eclipsefoundation.git.eca.api;
+package org.eclipsefoundation.git.eca.test.api;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -17,6 +17,7 @@ import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.git.eca.api.AccountsAPI;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
 import org.eclipsefoundation.git.eca.model.EclipseUser.ECA;
 
diff --git a/src/test/java/org/eclipsefoundation/git/eca/api/MockBotsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockBotsAPI.java
similarity index 96%
rename from src/test/java/org/eclipsefoundation/git/eca/api/MockBotsAPI.java
rename to src/test/java/org/eclipsefoundation/git/eca/test/api/MockBotsAPI.java
index d356ffc2..52bc7a66 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/api/MockBotsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockBotsAPI.java
@@ -7,7 +7,7 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
  ******************************************************************************/
-package org.eclipsefoundation.git.eca.api;
+package org.eclipsefoundation.git.eca.test.api;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -16,6 +16,7 @@ import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.git.eca.api.BotsAPI;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
diff --git a/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
similarity index 97%
rename from src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java
rename to src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
index e4c103b4..14737a9c 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/api/MockProjectsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
@@ -7,7 +7,7 @@
  * 
  * SPDX-License-Identifier: EPL-2.0
  ******************************************************************************/
-package org.eclipsefoundation.git.eca.api;
+package org.eclipsefoundation.git.eca.test.api;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -20,6 +20,7 @@ import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.git.eca.api.ProjectsAPI;
 import org.eclipsefoundation.git.eca.model.Project;
 import org.eclipsefoundation.git.eca.model.Project.Repo;
 import org.eclipsefoundation.git.eca.model.Project.User;
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java b/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java
new file mode 100644
index 00000000..bd99eadb
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/namespaces/SchemaNamespaceHelper.java
@@ -0,0 +1,10 @@
+package org.eclipsefoundation.git.eca.test.namespaces;
+
+public final class SchemaNamespaceHelper {
+    public static final String BASE_SCHEMAS_PATH = "schemas/";
+    public static final String BASE_SCHEMAS_PATH_SUFFIX = "-schema.json";
+    public static final String VALIDATION_REQUEST_SCHEMA_PATH = BASE_SCHEMAS_PATH + "validation-request"
+            + BASE_SCHEMAS_PATH_SUFFIX;
+    public static final String VALIDATION_RESPONSE_SCHEMA_PATH = BASE_SCHEMAS_PATH + "validation-response"
+            + BASE_SCHEMAS_PATH_SUFFIX;
+}
-- 
GitLab


From 9fffe3e5faf4a383aca37d059d5a12900e10d752 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 21 Apr 2022 10:38:43 -0400
Subject: [PATCH 04/11] Fix Java version to be java 11

---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index c1cc7de9..6f964e01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,8 +8,8 @@
 		<eclipse-api-version>0.6-SNAPSHOT</eclipse-api-version>
 		<compiler-plugin.version>3.8.1</compiler-plugin.version>
 		<maven.compiler.parameters>true</maven.compiler.parameters>
-		<maven.compiler.source>1.8</maven.compiler.source>
-		<maven.compiler.target>1.8</maven.compiler.target>
+		<maven.compiler.source>11</maven.compiler.source>
+		<maven.compiler.target>11</maven.compiler.target>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 		<quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
-- 
GitLab


From 71bedbc9916268752706cbad1322509e3120ab0d Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 21 Apr 2022 11:29:29 -0400
Subject: [PATCH 05/11] Update build to remove npm requirement and replace with
 yarn

---
 Jenkinsfile       |   90 +-
 Makefile          |   19 +
 package-lock.json | 2087 ---------------------------------------------
 pom.xml           |   50 --
 yarn.lock         |  750 ++++++++++++++++
 5 files changed, 847 insertions(+), 2149 deletions(-)
 create mode 100644 Makefile
 delete mode 100644 package-lock.json
 create mode 100644 yarn.lock

diff --git a/Jenkinsfile b/Jenkinsfile
index 14fb8629..84f86b8f 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -1,7 +1,77 @@
   @Library('common-shared') _
 
   pipeline {
-    agent any
+    agent {
+      kubernetes {
+        label 'buildenv-agent'
+        yaml '''
+        apiVersion: v1
+        kind: Pod
+        spec:
+          containers:
+          - name: buildcontainer
+            image: eclipsefdn/stack-build-agent:latest
+            imagePullPolicy: Always
+            command:
+            - cat
+            tty: true
+            resources:
+              requests:
+                cpu: "1"
+                memory: "4Gi"
+              limits:
+                cpu: "2"
+                memory: "4Gi"
+            env:
+            - name: "HOME"
+              value: "/home/jenkins"
+            - name: "MAVEN_OPTS"
+              value: "-Duser.home=/home/jenkins"
+            volumeMounts:
+            - name: m2-repo
+              mountPath: /home/jenkins/.m2/repository
+            - name: m2-secret-dir
+              mountPath: /home/jenkins/.m2/settings.xml
+              subPath: settings.xml
+              readOnly: true
+            - mountPath: "/home/jenkins/.m2/settings-security.xml"
+              name: "m2-secret-dir"
+              readOnly: true
+              subPath: "settings-security.xml"
+            - mountPath: "/home/jenkins/.mavenrc"
+              name: "m2-dir"
+              readOnly: true
+              subPath: ".mavenrc"
+            - mountPath: "/home/jenkins/.m2/wrapper"
+              name: "m2-wrapper"
+              readOnly: false
+            - mountPath: "/home/jenkins/.cache"
+              name: "yarn-cache"
+              readOnly: false
+          - name: jnlp
+            resources:
+              requests:
+                memory: "1024Mi"
+                cpu: "500m"
+              limits:
+                memory: "1024Mi"
+                cpu: "1000m"
+          volumes:
+          - name: "m2-dir"
+            configMap:
+              name: "m2-dir"
+          - name: m2-secret-dir
+            secret:
+              secretName: m2-secret-dir
+          - name: m2-repo
+            emptyDir: {}
+          - name: m2-wrapper
+            emptyDir: {}
+          - name: yarn-cache
+            emptyDir: {}
+        '''
+      }
+    }
 
     environment {
       APP_NAME = 'git-eca-rest-api'
@@ -10,11 +80,7 @@
       CONTAINER_NAME = 'app'
       ENVIRONMENT = sh(
         script: """
-          if [ "${env.BRANCH_NAME}" = "master" ]; then
-            printf "production"
-          else
-            printf "${env.BRANCH_NAME}"
-          fi
+          printf "${env.BRANCH_NAME}"
         """,
         returnStdout: true
       )
@@ -43,12 +109,12 @@
     stages {
       stage('Build Java code') {
         steps {
-          readTrusted 'mvnw'
-          readTrusted '.mvn/wrapper/MavenWrapperDownloader.java'
-          readTrusted 'pom.xml'
-
-          sh './mvnw -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn --batch-mode package'
-          stash includes: 'target/', name: 'target'
+          container('buildcontainer') {
+            readTrusted 'Makefile'
+            readTrusted 'pom.xml'
+            sh 'make compile'
+            stash name: "target", includes: "target/**/*"
+          }
         }
       }
 
diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000..9dd78172
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+clean:;
+	mvn clean
+compile-java: generate-spec;
+	mvn compile package
+compile-java-quick: generate-spec;
+	mvn compile package -Dmaven.test.skip=true
+compile: clean compile-java;
+compile-quick: clean compile-java-quick;
+install-yarn:;
+	yarn install --frozen-lockfile --audit
+generate-spec: install-yarn validate-spec;
+	yarn run generate-json-schema
+validate-spec: install-yarn;
+compile-start: compile-quick;
+	docker-compose down
+	docker-compose build
+	docker-compose up
+start-spec: validate-spec;
+	yarn run start
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
deleted file mode 100644
index e116e830..00000000
--- a/package-lock.json
+++ /dev/null
@@ -1,2087 +0,0 @@
-{
-    "name": "eclipsefdn-git-eca-rest-api-support",
-    "version": "1.0.0",
-    "lockfileVersion": 2,
-    "requires": true,
-    "packages": {
-        "": {
-            "name": "eclipsefdn-git-eca-rest-api-support",
-            "version": "1.0.0",
-            "devDependencies": {
-                "@openapi-contrib/openapi-schema-to-json-schema": "^3.1.1",
-                "@redocly/openapi-cli": "^1.0.0-beta.54",
-                "@stoplight/json-ref-resolver": "^3.1.2",
-                "decamelize": "^5.0.0",
-                "js-yaml": "^4.1.0",
-                "yargs": "^17.0.1"
-            }
-        },
-        "node_modules/@openapi-contrib/openapi-schema-to-json-schema": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz",
-            "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==",
-            "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.3"
-            }
-        },
-        "node_modules/@redocly/ajv": {
-            "version": "8.6.4",
-            "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.4.tgz",
-            "integrity": "sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==",
-            "dev": true,
-            "dependencies": {
-                "fast-deep-equal": "^3.1.1",
-                "json-schema-traverse": "^1.0.0",
-                "require-from-string": "^2.0.2",
-                "uri-js": "^4.2.2"
-            },
-            "funding": {
-                "type": "github",
-                "url": "https://github.com/sponsors/epoberezkin"
-            }
-        },
-        "node_modules/@redocly/openapi-cli": {
-            "version": "1.0.0-beta.80",
-            "resolved": "https://registry.npmjs.org/@redocly/openapi-cli/-/openapi-cli-1.0.0-beta.80.tgz",
-            "integrity": "sha512-PZLustQdB0ZsKjM5vgGxEg6eFf8okaky/LmXiicnNeJhXmlv7CmvytSvm6zixCwDts4DpuFUNO1CcDf7+iJyDA==",
-            "dev": true,
-            "dependencies": {
-                "@redocly/openapi-core": "1.0.0-beta.80",
-                "@types/node": "^14.11.8",
-                "assert-node-version": "^1.0.3",
-                "chokidar": "^3.5.1",
-                "colorette": "^1.2.0",
-                "glob": "^7.1.6",
-                "glob-promise": "^3.4.0",
-                "handlebars": "^4.7.6",
-                "portfinder": "^1.0.26",
-                "simple-websocket": "^9.0.0",
-                "yargs": "17.0.1"
-            },
-            "bin": {
-                "openapi": "bin/cli.js"
-            },
-            "engines": {
-                "node": ">=12.0.0"
-            }
-        },
-        "node_modules/@redocly/openapi-cli/node_modules/yargs": {
-            "version": "17.0.1",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz",
-            "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==",
-            "dev": true,
-            "dependencies": {
-                "cliui": "^7.0.2",
-                "escalade": "^3.1.1",
-                "get-caller-file": "^2.0.5",
-                "require-directory": "^2.1.1",
-                "string-width": "^4.2.0",
-                "y18n": "^5.0.5",
-                "yargs-parser": "^20.2.2"
-            },
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/@redocly/openapi-cli/node_modules/yargs-parser": {
-            "version": "20.2.9",
-            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
-            "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
-            "dev": true,
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/@redocly/openapi-core": {
-            "version": "1.0.0-beta.80",
-            "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz",
-            "integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==",
-            "dev": true,
-            "dependencies": {
-                "@redocly/ajv": "^8.6.4",
-                "@types/node": "^14.11.8",
-                "colorette": "^1.2.0",
-                "js-levenshtein": "^1.1.6",
-                "js-yaml": "^4.1.0",
-                "lodash.isequal": "^4.5.0",
-                "minimatch": "^3.0.4",
-                "node-fetch": "^2.6.1",
-                "pluralize": "^8.0.0",
-                "yaml-ast-parser": "0.0.43"
-            },
-            "engines": {
-                "node": ">=12.0.0"
-            }
-        },
-        "node_modules/@stoplight/json": {
-            "version": "3.17.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.17.2.tgz",
-            "integrity": "sha512-NwIVzanXRUy291J5BMkncCZRMG1Lx+aq+VidGQgfkJjgo8vh1Y/PSAz7fSU8gVGSZBCcqmOkMI7R4zw7DlfTwA==",
-            "dev": true,
-            "dependencies": {
-                "@stoplight/ordered-object-literal": "^1.0.2",
-                "@stoplight/types": "^12.3.0",
-                "jsonc-parser": "~2.2.1",
-                "lodash": "^4.17.21",
-                "safe-stable-stringify": "^1.1"
-            },
-            "engines": {
-                "node": ">=8.3.0"
-            }
-        },
-        "node_modules/@stoplight/json-ref-resolver": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.3.tgz",
-            "integrity": "sha512-SgoKXwVnlpIZUyAFX4W79eeuTWvXmNlMfICZixL16GZXnkjcW+uZnfmAU0ZIjcnaTgaI4mjfxn8LAP2KR6Cr0A==",
-            "dev": true,
-            "dependencies": {
-                "@stoplight/json": "^3.17.0",
-                "@stoplight/path": "^1.3.2",
-                "@stoplight/types": "^12.3.0",
-                "@types/urijs": "^1.19.16",
-                "dependency-graph": "~0.11.0",
-                "fast-memoize": "^2.5.2",
-                "immer": "^9.0.6",
-                "lodash.get": "^4.4.2",
-                "lodash.set": "^4.3.2",
-                "tslib": "^2.3.1",
-                "urijs": "^1.19.6"
-            },
-            "engines": {
-                "node": ">=8.3.0"
-            }
-        },
-        "node_modules/@stoplight/ordered-object-literal": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz",
-            "integrity": "sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/@stoplight/path": {
-            "version": "1.3.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz",
-            "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/@stoplight/types": {
-            "version": "12.5.0",
-            "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-12.5.0.tgz",
-            "integrity": "sha512-dwqYcDrGmEyUv5TWrDam5TGOxU72ufyQ7hnOIIDdmW5ezOwZaBFoR5XQ9AsH49w7wgvOqB2Bmo799pJPWnpCbg==",
-            "dev": true,
-            "dependencies": {
-                "@types/json-schema": "^7.0.4",
-                "utility-types": "^3.10.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/@types/glob": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
-            "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
-            "dev": true,
-            "dependencies": {
-                "@types/minimatch": "*",
-                "@types/node": "*"
-            }
-        },
-        "node_modules/@types/json-schema": {
-            "version": "7.0.9",
-            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
-            "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
-            "dev": true
-        },
-        "node_modules/@types/minimatch": {
-            "version": "3.0.5",
-            "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
-            "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
-            "dev": true
-        },
-        "node_modules/@types/node": {
-            "version": "14.18.9",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
-            "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==",
-            "dev": true
-        },
-        "node_modules/@types/urijs": {
-            "version": "1.19.18",
-            "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.18.tgz",
-            "integrity": "sha512-tjftsOLuIWFLJxcpgFeehNnMhpMIv0ELJl0/i31jiV3au1GQpnd3/pTTDQg2zO5cSGJxtrDzMgebOH7+cqh3Vg==",
-            "dev": true
-        },
-        "node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/anymatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
-            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
-            "dev": true,
-            "dependencies": {
-                "normalize-path": "^3.0.0",
-                "picomatch": "^2.0.4"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
-        "node_modules/argparse": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-            "dev": true
-        },
-        "node_modules/assert-node-version": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/assert-node-version/-/assert-node-version-1.0.3.tgz",
-            "integrity": "sha1-yupdG2pY285ZZhII3x4bnkxYD5E=",
-            "dev": true,
-            "dependencies": {
-                "expected-node-version": "^1.0.0",
-                "semver": "^5.0.3"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/async": {
-            "version": "2.6.3",
-            "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
-            "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
-            "dev": true,
-            "dependencies": {
-                "lodash": "^4.17.14"
-            }
-        },
-        "node_modules/balanced-match": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-            "dev": true
-        },
-        "node_modules/binary-extensions": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/braces": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-            "dev": true,
-            "dependencies": {
-                "fill-range": "^7.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/chokidar": {
-            "version": "3.5.3",
-            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "individual",
-                    "url": "https://paulmillr.com/funding/"
-                }
-            ],
-            "dependencies": {
-                "anymatch": "~3.1.2",
-                "braces": "~3.0.2",
-                "glob-parent": "~5.1.2",
-                "is-binary-path": "~2.1.0",
-                "is-glob": "~4.0.1",
-                "normalize-path": "~3.0.0",
-                "readdirp": "~3.6.0"
-            },
-            "engines": {
-                "node": ">= 8.10.0"
-            },
-            "optionalDependencies": {
-                "fsevents": "~2.3.2"
-            }
-        },
-        "node_modules/cliui": {
-            "version": "7.0.4",
-            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
-            "dev": true,
-            "dependencies": {
-                "string-width": "^4.2.0",
-                "strip-ansi": "^6.0.0",
-                "wrap-ansi": "^7.0.0"
-            }
-        },
-        "node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
-        },
-        "node_modules/colorette": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
-            "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
-            "dev": true
-        },
-        "node_modules/concat-map": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-            "dev": true
-        },
-        "node_modules/debug": {
-            "version": "3.2.7",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-            "dev": true,
-            "dependencies": {
-                "ms": "^2.1.1"
-            }
-        },
-        "node_modules/decamelize": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
-            "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
-            "dev": true,
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/dependency-graph": {
-            "version": "0.11.0",
-            "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
-            "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
-            "dev": true,
-            "engines": {
-                "node": ">= 0.6.0"
-            }
-        },
-        "node_modules/emoji-regex": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
-        },
-        "node_modules/escalade": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-            "dev": true,
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/expected-node-version": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/expected-node-version/-/expected-node-version-1.0.2.tgz",
-            "integrity": "sha1-uNIlub9nap6H4G29YVtS/J0eOGs=",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/fast-deep-equal": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true
-        },
-        "node_modules/fast-memoize": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
-            "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==",
-            "dev": true
-        },
-        "node_modules/fill-range": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-            "dev": true,
-            "dependencies": {
-                "to-regex-range": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/fs.realpath": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-            "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
-            "dev": true
-        },
-        "node_modules/get-caller-file": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-            "dev": true,
-            "engines": {
-                "node": "6.* || 8.* || >= 10.*"
-            }
-        },
-        "node_modules/glob": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-            "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
-            "dev": true,
-            "dependencies": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.0.4",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            },
-            "engines": {
-                "node": "*"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/isaacs"
-            }
-        },
-        "node_modules/glob-parent": {
-            "version": "5.1.2",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-            "dev": true,
-            "dependencies": {
-                "is-glob": "^4.0.1"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/glob-promise": {
-            "version": "3.4.0",
-            "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz",
-            "integrity": "sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==",
-            "dev": true,
-            "dependencies": {
-                "@types/glob": "*"
-            },
-            "engines": {
-                "node": ">=4"
-            },
-            "peerDependencies": {
-                "glob": "*"
-            }
-        },
-        "node_modules/handlebars": {
-            "version": "4.7.7",
-            "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
-            "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
-            "dev": true,
-            "dependencies": {
-                "minimist": "^1.2.5",
-                "neo-async": "^2.6.0",
-                "source-map": "^0.6.1",
-                "wordwrap": "^1.0.0"
-            },
-            "bin": {
-                "handlebars": "bin/handlebars"
-            },
-            "engines": {
-                "node": ">=0.4.7"
-            },
-            "optionalDependencies": {
-                "uglify-js": "^3.1.4"
-            }
-        },
-        "node_modules/immer": {
-            "version": "9.0.12",
-            "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
-            "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
-            "dev": true,
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/immer"
-            }
-        },
-        "node_modules/inflight": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-            "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-            "dev": true,
-            "dependencies": {
-                "once": "^1.3.0",
-                "wrappy": "1"
-            }
-        },
-        "node_modules/inherits": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-            "dev": true
-        },
-        "node_modules/is-binary-path": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-            "dev": true,
-            "dependencies": {
-                "binary-extensions": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-extglob": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-fullwidth-code-point": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-            "dev": true,
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-glob": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-            "dev": true,
-            "dependencies": {
-                "is-extglob": "^2.1.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-number": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.12.0"
-            }
-        },
-        "node_modules/js-levenshtein": {
-            "version": "1.1.6",
-            "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
-            "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-            "dev": true,
-            "dependencies": {
-                "argparse": "^2.0.1"
-            },
-            "bin": {
-                "js-yaml": "bin/js-yaml.js"
-            }
-        },
-        "node_modules/json-schema-traverse": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-            "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-            "dev": true
-        },
-        "node_modules/jsonc-parser": {
-            "version": "2.2.1",
-            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz",
-            "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==",
-            "dev": true
-        },
-        "node_modules/lodash": {
-            "version": "4.17.21",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-            "dev": true
-        },
-        "node_modules/lodash.get": {
-            "version": "4.4.2",
-            "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-            "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-            "dev": true
-        },
-        "node_modules/lodash.isequal": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
-            "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
-            "dev": true
-        },
-        "node_modules/lodash.set": {
-            "version": "4.3.2",
-            "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-            "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-            "dev": true
-        },
-        "node_modules/minimatch": {
-            "version": "3.0.4",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-            "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-            "dev": true,
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/minimist": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-            "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-            "dev": true
-        },
-        "node_modules/mkdirp": {
-            "version": "0.5.5",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
-            "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-            "dev": true,
-            "dependencies": {
-                "minimist": "^1.2.5"
-            },
-            "bin": {
-                "mkdirp": "bin/cmd.js"
-            }
-        },
-        "node_modules/ms": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-            "dev": true
-        },
-        "node_modules/neo-async": {
-            "version": "2.6.2",
-            "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
-            "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
-            "dev": true
-        },
-        "node_modules/node-fetch": {
-            "version": "2.6.7",
-            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-            "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-            "dev": true,
-            "dependencies": {
-                "whatwg-url": "^5.0.0"
-            },
-            "engines": {
-                "node": "4.x || >=6.0.0"
-            },
-            "peerDependencies": {
-                "encoding": "^0.1.0"
-            },
-            "peerDependenciesMeta": {
-                "encoding": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/normalize-path": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "dev": true,
-            "dependencies": {
-                "wrappy": "1"
-            }
-        },
-        "node_modules/path-is-absolute": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-            "dev": true,
-            "engines": {
-                "node": ">=8.6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/jonschlinkert"
-            }
-        },
-        "node_modules/pluralize": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
-            "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
-            "dev": true,
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/portfinder": {
-            "version": "1.0.28",
-            "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
-            "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
-            "dev": true,
-            "dependencies": {
-                "async": "^2.6.2",
-                "debug": "^3.1.1",
-                "mkdirp": "^0.5.5"
-            },
-            "engines": {
-                "node": ">= 0.12.0"
-            }
-        },
-        "node_modules/punycode": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-            "dev": true,
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/queue-microtask": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ]
-        },
-        "node_modules/randombytes": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
-            "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-            "dev": true,
-            "dependencies": {
-                "safe-buffer": "^5.1.0"
-            }
-        },
-        "node_modules/readable-stream": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
-            "dev": true,
-            "dependencies": {
-                "inherits": "^2.0.3",
-                "string_decoder": "^1.1.1",
-                "util-deprecate": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
-        "node_modules/readdirp": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-            "dev": true,
-            "dependencies": {
-                "picomatch": "^2.2.1"
-            },
-            "engines": {
-                "node": ">=8.10.0"
-            }
-        },
-        "node_modules/require-directory": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-            "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/require-from-string": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
-            "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/safe-buffer": {
-            "version": "5.2.1",
-            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ]
-        },
-        "node_modules/safe-stable-stringify": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
-            "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==",
-            "dev": true
-        },
-        "node_modules/semver": {
-            "version": "5.7.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-            "dev": true,
-            "bin": {
-                "semver": "bin/semver"
-            }
-        },
-        "node_modules/simple-websocket": {
-            "version": "9.1.0",
-            "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
-            "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==",
-            "dev": true,
-            "funding": [
-                {
-                    "type": "github",
-                    "url": "https://github.com/sponsors/feross"
-                },
-                {
-                    "type": "patreon",
-                    "url": "https://www.patreon.com/feross"
-                },
-                {
-                    "type": "consulting",
-                    "url": "https://feross.org/support"
-                }
-            ],
-            "dependencies": {
-                "debug": "^4.3.1",
-                "queue-microtask": "^1.2.2",
-                "randombytes": "^2.1.0",
-                "readable-stream": "^3.6.0",
-                "ws": "^7.4.2"
-            }
-        },
-        "node_modules/simple-websocket/node_modules/debug": {
-            "version": "4.3.3",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
-            "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
-            "dev": true,
-            "dependencies": {
-                "ms": "2.1.2"
-            },
-            "engines": {
-                "node": ">=6.0"
-            },
-            "peerDependenciesMeta": {
-                "supports-color": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/simple-websocket/node_modules/ms": {
-            "version": "2.1.2",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-            "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-            "dev": true
-        },
-        "node_modules/source-map": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-            "dev": true,
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/string_decoder": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-            "dev": true,
-            "dependencies": {
-                "safe-buffer": "~5.2.0"
-            }
-        },
-        "node_modules/string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
-            "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/to-regex-range": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-            "dev": true,
-            "dependencies": {
-                "is-number": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=8.0"
-            }
-        },
-        "node_modules/tr46": {
-            "version": "0.0.3",
-            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-            "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-            "dev": true
-        },
-        "node_modules/tslib": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-            "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
-            "dev": true
-        },
-        "node_modules/uglify-js": {
-            "version": "3.14.5",
-            "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz",
-            "integrity": "sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ==",
-            "dev": true,
-            "optional": true,
-            "bin": {
-                "uglifyjs": "bin/uglifyjs"
-            },
-            "engines": {
-                "node": ">=0.8.0"
-            }
-        },
-        "node_modules/uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "dependencies": {
-                "punycode": "^2.1.0"
-            }
-        },
-        "node_modules/urijs": {
-            "version": "1.19.7",
-            "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz",
-            "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==",
-            "dev": true
-        },
-        "node_modules/util-deprecate": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-            "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-            "dev": true
-        },
-        "node_modules/utility-types": {
-            "version": "3.10.0",
-            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
-            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
-            "dev": true,
-            "engines": {
-                "node": ">= 4"
-            }
-        },
-        "node_modules/webidl-conversions": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-            "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-            "dev": true
-        },
-        "node_modules/whatwg-url": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-            "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-            "dev": true,
-            "dependencies": {
-                "tr46": "~0.0.3",
-                "webidl-conversions": "^3.0.0"
-            }
-        },
-        "node_modules/wordwrap": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-            "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
-            "dev": true
-        },
-        "node_modules/wrap-ansi": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dev": true,
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-            }
-        },
-        "node_modules/wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-            "dev": true
-        },
-        "node_modules/ws": {
-            "version": "7.5.6",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
-            "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
-            "dev": true,
-            "engines": {
-                "node": ">=8.3.0"
-            },
-            "peerDependencies": {
-                "bufferutil": "^4.0.1",
-                "utf-8-validate": "^5.0.2"
-            },
-            "peerDependenciesMeta": {
-                "bufferutil": {
-                    "optional": true
-                },
-                "utf-8-validate": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/y18n": {
-            "version": "5.0.8",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
-            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
-            "dev": true,
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/yaml-ast-parser": {
-            "version": "0.0.43",
-            "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
-            "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
-            "dev": true
-        },
-        "node_modules/yargs": {
-            "version": "17.3.1",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
-            "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
-            "dev": true,
-            "dependencies": {
-                "cliui": "^7.0.2",
-                "escalade": "^3.1.1",
-                "get-caller-file": "^2.0.5",
-                "require-directory": "^2.1.1",
-                "string-width": "^4.2.3",
-                "y18n": "^5.0.5",
-                "yargs-parser": "^21.0.0"
-            },
-            "engines": {
-                "node": ">=12"
-            }
-        },
-        "node_modules/yargs-parser": {
-            "version": "21.0.0",
-            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
-            "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
-            "dev": true,
-            "engines": {
-                "node": ">=12"
-            }
-        }
-    },
-    "dependencies": {
-        "@openapi-contrib/openapi-schema-to-json-schema": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz",
-            "integrity": "sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==",
-            "dev": true,
-            "requires": {
-                "fast-deep-equal": "^3.1.3"
-            }
-        },
-        "@redocly/ajv": {
-            "version": "8.6.4",
-            "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.6.4.tgz",
-            "integrity": "sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==",
-            "dev": true,
-            "requires": {
-                "fast-deep-equal": "^3.1.1",
-                "json-schema-traverse": "^1.0.0",
-                "require-from-string": "^2.0.2",
-                "uri-js": "^4.2.2"
-            }
-        },
-        "@redocly/openapi-cli": {
-            "version": "1.0.0-beta.80",
-            "resolved": "https://registry.npmjs.org/@redocly/openapi-cli/-/openapi-cli-1.0.0-beta.80.tgz",
-            "integrity": "sha512-PZLustQdB0ZsKjM5vgGxEg6eFf8okaky/LmXiicnNeJhXmlv7CmvytSvm6zixCwDts4DpuFUNO1CcDf7+iJyDA==",
-            "dev": true,
-            "requires": {
-                "@redocly/openapi-core": "1.0.0-beta.80",
-                "@types/node": "^14.11.8",
-                "assert-node-version": "^1.0.3",
-                "chokidar": "^3.5.1",
-                "colorette": "^1.2.0",
-                "glob": "^7.1.6",
-                "glob-promise": "^3.4.0",
-                "handlebars": "^4.7.6",
-                "portfinder": "^1.0.26",
-                "simple-websocket": "^9.0.0",
-                "yargs": "17.0.1"
-            },
-            "dependencies": {
-                "yargs": {
-                    "version": "17.0.1",
-                    "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.1.tgz",
-                    "integrity": "sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==",
-                    "dev": true,
-                    "requires": {
-                        "cliui": "^7.0.2",
-                        "escalade": "^3.1.1",
-                        "get-caller-file": "^2.0.5",
-                        "require-directory": "^2.1.1",
-                        "string-width": "^4.2.0",
-                        "y18n": "^5.0.5",
-                        "yargs-parser": "^20.2.2"
-                    }
-                },
-                "yargs-parser": {
-                    "version": "20.2.9",
-                    "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
-                    "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
-                    "dev": true
-                }
-            }
-        },
-        "@redocly/openapi-core": {
-            "version": "1.0.0-beta.80",
-            "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.0.0-beta.80.tgz",
-            "integrity": "sha512-IAQECLt/fDxjlfNdLGnJszt40BaiA6b78+zB6+7Rk8ums0HHLfwWFJPMTzh1bzJ5f+sZ4zDBi4gaIJ1n4XGCHg==",
-            "dev": true,
-            "requires": {
-                "@redocly/ajv": "^8.6.4",
-                "@types/node": "^14.11.8",
-                "colorette": "^1.2.0",
-                "js-levenshtein": "^1.1.6",
-                "js-yaml": "^4.1.0",
-                "lodash.isequal": "^4.5.0",
-                "minimatch": "^3.0.4",
-                "node-fetch": "^2.6.1",
-                "pluralize": "^8.0.0",
-                "yaml-ast-parser": "0.0.43"
-            }
-        },
-        "@stoplight/json": {
-            "version": "3.17.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/json/-/json-3.17.2.tgz",
-            "integrity": "sha512-NwIVzanXRUy291J5BMkncCZRMG1Lx+aq+VidGQgfkJjgo8vh1Y/PSAz7fSU8gVGSZBCcqmOkMI7R4zw7DlfTwA==",
-            "dev": true,
-            "requires": {
-                "@stoplight/ordered-object-literal": "^1.0.2",
-                "@stoplight/types": "^12.3.0",
-                "jsonc-parser": "~2.2.1",
-                "lodash": "^4.17.21",
-                "safe-stable-stringify": "^1.1"
-            }
-        },
-        "@stoplight/json-ref-resolver": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.3.tgz",
-            "integrity": "sha512-SgoKXwVnlpIZUyAFX4W79eeuTWvXmNlMfICZixL16GZXnkjcW+uZnfmAU0ZIjcnaTgaI4mjfxn8LAP2KR6Cr0A==",
-            "dev": true,
-            "requires": {
-                "@stoplight/json": "^3.17.0",
-                "@stoplight/path": "^1.3.2",
-                "@stoplight/types": "^12.3.0",
-                "@types/urijs": "^1.19.16",
-                "dependency-graph": "~0.11.0",
-                "fast-memoize": "^2.5.2",
-                "immer": "^9.0.6",
-                "lodash.get": "^4.4.2",
-                "lodash.set": "^4.3.2",
-                "tslib": "^2.3.1",
-                "urijs": "^1.19.6"
-            }
-        },
-        "@stoplight/ordered-object-literal": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz",
-            "integrity": "sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==",
-            "dev": true
-        },
-        "@stoplight/path": {
-            "version": "1.3.2",
-            "resolved": "https://registry.npmjs.org/@stoplight/path/-/path-1.3.2.tgz",
-            "integrity": "sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==",
-            "dev": true
-        },
-        "@stoplight/types": {
-            "version": "12.5.0",
-            "resolved": "https://registry.npmjs.org/@stoplight/types/-/types-12.5.0.tgz",
-            "integrity": "sha512-dwqYcDrGmEyUv5TWrDam5TGOxU72ufyQ7hnOIIDdmW5ezOwZaBFoR5XQ9AsH49w7wgvOqB2Bmo799pJPWnpCbg==",
-            "dev": true,
-            "requires": {
-                "@types/json-schema": "^7.0.4",
-                "utility-types": "^3.10.0"
-            }
-        },
-        "@types/glob": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
-            "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==",
-            "dev": true,
-            "requires": {
-                "@types/minimatch": "*",
-                "@types/node": "*"
-            }
-        },
-        "@types/json-schema": {
-            "version": "7.0.9",
-            "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
-            "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
-            "dev": true
-        },
-        "@types/minimatch": {
-            "version": "3.0.5",
-            "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
-            "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
-            "dev": true
-        },
-        "@types/node": {
-            "version": "14.18.9",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.9.tgz",
-            "integrity": "sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==",
-            "dev": true
-        },
-        "@types/urijs": {
-            "version": "1.19.18",
-            "resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.19.18.tgz",
-            "integrity": "sha512-tjftsOLuIWFLJxcpgFeehNnMhpMIv0ELJl0/i31jiV3au1GQpnd3/pTTDQg2zO5cSGJxtrDzMgebOH7+cqh3Vg==",
-            "dev": true
-        },
-        "ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "dev": true
-        },
-        "ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dev": true,
-            "requires": {
-                "color-convert": "^2.0.1"
-            }
-        },
-        "anymatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
-            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
-            "dev": true,
-            "requires": {
-                "normalize-path": "^3.0.0",
-                "picomatch": "^2.0.4"
-            }
-        },
-        "argparse": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
-            "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
-            "dev": true
-        },
-        "assert-node-version": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/assert-node-version/-/assert-node-version-1.0.3.tgz",
-            "integrity": "sha1-yupdG2pY285ZZhII3x4bnkxYD5E=",
-            "dev": true,
-            "requires": {
-                "expected-node-version": "^1.0.0",
-                "semver": "^5.0.3"
-            }
-        },
-        "async": {
-            "version": "2.6.3",
-            "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
-            "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
-            "dev": true,
-            "requires": {
-                "lodash": "^4.17.14"
-            }
-        },
-        "balanced-match": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-            "dev": true
-        },
-        "binary-extensions": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-            "dev": true
-        },
-        "brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dev": true,
-            "requires": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "braces": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-            "dev": true,
-            "requires": {
-                "fill-range": "^7.0.1"
-            }
-        },
-        "chokidar": {
-            "version": "3.5.3",
-            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-            "dev": true,
-            "requires": {
-                "anymatch": "~3.1.2",
-                "braces": "~3.0.2",
-                "fsevents": "~2.3.2",
-                "glob-parent": "~5.1.2",
-                "is-binary-path": "~2.1.0",
-                "is-glob": "~4.0.1",
-                "normalize-path": "~3.0.0",
-                "readdirp": "~3.6.0"
-            }
-        },
-        "cliui": {
-            "version": "7.0.4",
-            "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-            "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
-            "dev": true,
-            "requires": {
-                "string-width": "^4.2.0",
-                "strip-ansi": "^6.0.0",
-                "wrap-ansi": "^7.0.0"
-            }
-        },
-        "color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dev": true,
-            "requires": {
-                "color-name": "~1.1.4"
-            }
-        },
-        "color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-            "dev": true
-        },
-        "colorette": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
-            "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
-            "dev": true
-        },
-        "concat-map": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-            "dev": true
-        },
-        "debug": {
-            "version": "3.2.7",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-            "dev": true,
-            "requires": {
-                "ms": "^2.1.1"
-            }
-        },
-        "decamelize": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz",
-            "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==",
-            "dev": true
-        },
-        "dependency-graph": {
-            "version": "0.11.0",
-            "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
-            "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
-            "dev": true
-        },
-        "emoji-regex": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-            "dev": true
-        },
-        "escalade": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-            "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
-            "dev": true
-        },
-        "expected-node-version": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/expected-node-version/-/expected-node-version-1.0.2.tgz",
-            "integrity": "sha1-uNIlub9nap6H4G29YVtS/J0eOGs=",
-            "dev": true
-        },
-        "fast-deep-equal": {
-            "version": "3.1.3",
-            "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-            "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-            "dev": true
-        },
-        "fast-memoize": {
-            "version": "2.5.2",
-            "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz",
-            "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==",
-            "dev": true
-        },
-        "fill-range": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
-            "dev": true,
-            "requires": {
-                "to-regex-range": "^5.0.1"
-            }
-        },
-        "fs.realpath": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-            "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
-            "dev": true
-        },
-        "get-caller-file": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
-            "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-            "dev": true
-        },
-        "glob": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-            "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
-            "dev": true,
-            "requires": {
-                "fs.realpath": "^1.0.0",
-                "inflight": "^1.0.4",
-                "inherits": "2",
-                "minimatch": "^3.0.4",
-                "once": "^1.3.0",
-                "path-is-absolute": "^1.0.0"
-            }
-        },
-        "glob-parent": {
-            "version": "5.1.2",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-            "dev": true,
-            "requires": {
-                "is-glob": "^4.0.1"
-            }
-        },
-        "glob-promise": {
-            "version": "3.4.0",
-            "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-3.4.0.tgz",
-            "integrity": "sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==",
-            "dev": true,
-            "requires": {
-                "@types/glob": "*"
-            }
-        },
-        "handlebars": {
-            "version": "4.7.7",
-            "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz",
-            "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==",
-            "dev": true,
-            "requires": {
-                "minimist": "^1.2.5",
-                "neo-async": "^2.6.0",
-                "source-map": "^0.6.1",
-                "uglify-js": "^3.1.4",
-                "wordwrap": "^1.0.0"
-            }
-        },
-        "immer": {
-            "version": "9.0.12",
-            "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz",
-            "integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
-            "dev": true
-        },
-        "inflight": {
-            "version": "1.0.6",
-            "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-            "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-            "dev": true,
-            "requires": {
-                "once": "^1.3.0",
-                "wrappy": "1"
-            }
-        },
-        "inherits": {
-            "version": "2.0.4",
-            "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-            "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-            "dev": true
-        },
-        "is-binary-path": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-            "dev": true,
-            "requires": {
-                "binary-extensions": "^2.0.0"
-            }
-        },
-        "is-extglob": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-            "dev": true
-        },
-        "is-fullwidth-code-point": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-            "dev": true
-        },
-        "is-glob": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-            "dev": true,
-            "requires": {
-                "is-extglob": "^2.1.1"
-            }
-        },
-        "is-number": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-            "dev": true
-        },
-        "js-levenshtein": {
-            "version": "1.1.6",
-            "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
-            "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
-            "dev": true
-        },
-        "js-yaml": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-            "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
-            "dev": true,
-            "requires": {
-                "argparse": "^2.0.1"
-            }
-        },
-        "json-schema-traverse": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
-            "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
-            "dev": true
-        },
-        "jsonc-parser": {
-            "version": "2.2.1",
-            "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz",
-            "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==",
-            "dev": true
-        },
-        "lodash": {
-            "version": "4.17.21",
-            "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-            "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-            "dev": true
-        },
-        "lodash.get": {
-            "version": "4.4.2",
-            "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
-            "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
-            "dev": true
-        },
-        "lodash.isequal": {
-            "version": "4.5.0",
-            "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
-            "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
-            "dev": true
-        },
-        "lodash.set": {
-            "version": "4.3.2",
-            "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
-            "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=",
-            "dev": true
-        },
-        "minimatch": {
-            "version": "3.0.4",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-            "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-            "dev": true,
-            "requires": {
-                "brace-expansion": "^1.1.7"
-            }
-        },
-        "minimist": {
-            "version": "1.2.5",
-            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-            "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-            "dev": true
-        },
-        "mkdirp": {
-            "version": "0.5.5",
-            "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
-            "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-            "dev": true,
-            "requires": {
-                "minimist": "^1.2.5"
-            }
-        },
-        "ms": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-            "dev": true
-        },
-        "neo-async": {
-            "version": "2.6.2",
-            "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
-            "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
-            "dev": true
-        },
-        "node-fetch": {
-            "version": "2.6.7",
-            "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
-            "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
-            "dev": true,
-            "requires": {
-                "whatwg-url": "^5.0.0"
-            }
-        },
-        "normalize-path": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-            "dev": true
-        },
-        "once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "dev": true,
-            "requires": {
-                "wrappy": "1"
-            }
-        },
-        "path-is-absolute": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-            "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-            "dev": true
-        },
-        "picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-            "dev": true
-        },
-        "pluralize": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
-            "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
-            "dev": true
-        },
-        "portfinder": {
-            "version": "1.0.28",
-            "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz",
-            "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==",
-            "dev": true,
-            "requires": {
-                "async": "^2.6.2",
-                "debug": "^3.1.1",
-                "mkdirp": "^0.5.5"
-            }
-        },
-        "punycode": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-            "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-            "dev": true
-        },
-        "queue-microtask": {
-            "version": "1.2.3",
-            "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
-            "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
-            "dev": true
-        },
-        "randombytes": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
-            "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
-            "dev": true,
-            "requires": {
-                "safe-buffer": "^5.1.0"
-            }
-        },
-        "readable-stream": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-            "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
-            "dev": true,
-            "requires": {
-                "inherits": "^2.0.3",
-                "string_decoder": "^1.1.1",
-                "util-deprecate": "^1.0.1"
-            }
-        },
-        "readdirp": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-            "dev": true,
-            "requires": {
-                "picomatch": "^2.2.1"
-            }
-        },
-        "require-directory": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
-            "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
-            "dev": true
-        },
-        "require-from-string": {
-            "version": "2.0.2",
-            "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
-            "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
-            "dev": true
-        },
-        "safe-buffer": {
-            "version": "5.2.1",
-            "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
-            "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
-            "dev": true
-        },
-        "safe-stable-stringify": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz",
-            "integrity": "sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==",
-            "dev": true
-        },
-        "semver": {
-            "version": "5.7.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-            "dev": true
-        },
-        "simple-websocket": {
-            "version": "9.1.0",
-            "resolved": "https://registry.npmjs.org/simple-websocket/-/simple-websocket-9.1.0.tgz",
-            "integrity": "sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==",
-            "dev": true,
-            "requires": {
-                "debug": "^4.3.1",
-                "queue-microtask": "^1.2.2",
-                "randombytes": "^2.1.0",
-                "readable-stream": "^3.6.0",
-                "ws": "^7.4.2"
-            },
-            "dependencies": {
-                "debug": {
-                    "version": "4.3.3",
-                    "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
-                    "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
-                    "dev": true,
-                    "requires": {
-                        "ms": "2.1.2"
-                    }
-                },
-                "ms": {
-                    "version": "2.1.2",
-                    "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
-                    "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
-                    "dev": true
-                }
-            }
-        },
-        "source-map": {
-            "version": "0.6.1",
-            "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
-            "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-            "dev": true
-        },
-        "string_decoder": {
-            "version": "1.3.0",
-            "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-            "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-            "dev": true,
-            "requires": {
-                "safe-buffer": "~5.2.0"
-            }
-        },
-        "string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dev": true,
-            "requires": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
-            }
-        },
-        "strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dev": true,
-            "requires": {
-                "ansi-regex": "^5.0.1"
-            }
-        },
-        "to-regex-range": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-            "dev": true,
-            "requires": {
-                "is-number": "^7.0.0"
-            }
-        },
-        "tr46": {
-            "version": "0.0.3",
-            "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
-            "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=",
-            "dev": true
-        },
-        "tslib": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
-            "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
-            "dev": true
-        },
-        "uglify-js": {
-            "version": "3.14.5",
-            "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz",
-            "integrity": "sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ==",
-            "dev": true,
-            "optional": true
-        },
-        "uri-js": {
-            "version": "4.4.1",
-            "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
-            "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-            "dev": true,
-            "requires": {
-                "punycode": "^2.1.0"
-            }
-        },
-        "urijs": {
-            "version": "1.19.7",
-            "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.7.tgz",
-            "integrity": "sha512-Id+IKjdU0Hx+7Zx717jwLPsPeUqz7rAtuVBRLLs+qn+J2nf9NGITWVCxcijgYxBqe83C7sqsQPs6H1pyz3x9gA==",
-            "dev": true
-        },
-        "util-deprecate": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-            "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-            "dev": true
-        },
-        "utility-types": {
-            "version": "3.10.0",
-            "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
-            "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==",
-            "dev": true
-        },
-        "webidl-conversions": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
-            "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=",
-            "dev": true
-        },
-        "whatwg-url": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
-            "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=",
-            "dev": true,
-            "requires": {
-                "tr46": "~0.0.3",
-                "webidl-conversions": "^3.0.0"
-            }
-        },
-        "wordwrap": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
-            "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=",
-            "dev": true
-        },
-        "wrap-ansi": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dev": true,
-            "requires": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
-            }
-        },
-        "wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-            "dev": true
-        },
-        "ws": {
-            "version": "7.5.6",
-            "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
-            "integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
-            "dev": true,
-            "requires": {}
-        },
-        "y18n": {
-            "version": "5.0.8",
-            "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
-            "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
-            "dev": true
-        },
-        "yaml-ast-parser": {
-            "version": "0.0.43",
-            "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
-            "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
-            "dev": true
-        },
-        "yargs": {
-            "version": "17.3.1",
-            "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz",
-            "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==",
-            "dev": true,
-            "requires": {
-                "cliui": "^7.0.2",
-                "escalade": "^3.1.1",
-                "get-caller-file": "^2.0.5",
-                "require-directory": "^2.1.1",
-                "string-width": "^4.2.3",
-                "y18n": "^5.0.5",
-                "yargs-parser": "^21.0.0"
-            }
-        },
-        "yargs-parser": {
-            "version": "21.0.0",
-            "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz",
-            "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==",
-            "dev": true
-        }
-    }
-}
diff --git a/pom.xml b/pom.xml
index 6f964e01..3a9eed7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -152,56 +152,6 @@
 					</annotationProcessorPaths>
 				</configuration>
 			</plugin>
-			<plugin>
-				<groupId>org.codehaus.mojo</groupId>
-				<artifactId>exec-maven-plugin</artifactId>
-				<version>1.3.2</version>
-				<executions>
-					<execution>
-						<id>npm install (initialize)</id>
-						<goals>
-							<goal>exec</goal>
-						</goals>
-						<phase>initialize</phase>
-						<configuration>
-							<executable>npm</executable>
-							<arguments>
-								<argument>install</argument>
-								<argument>-f</argument>
-							</arguments>
-						</configuration>
-					</execution>
-					<execution>
-						<id>npm clean</id>
-						<goals>
-							<goal>exec</goal>
-						</goals>
-						<phase>clean</phase>
-						<configuration>
-							<executable>npm</executable>
-							<arguments>
-								<argument>run</argument>
-								<argument>clean</argument>
-							</arguments>
-						</configuration>
-					</execution>
-					<execution>
-						<id>npm run pre-test</id>
-						<goals>
-							<goal>exec</goal>
-						</goals>
-						<phase>generate-test-resources</phase>
-						<configuration>
-							<executable>npm</executable>
-							<skip>${maven.test.skip}</skip>
-							<arguments>
-								<argument>run</argument>
-								<argument>generate-json-schema</argument>
-							</arguments>
-						</configuration>
-					</execution>
-				</executions>
-			</plugin>
 			<plugin>
 				<artifactId>maven-surefire-plugin</artifactId>
 				<version>${surefire-plugin.version}</version>
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 00000000..2076f7df
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,750 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@openapi-contrib/openapi-schema-to-json-schema@^3.1.1":
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/@openapi-contrib/openapi-schema-to-json-schema/-/openapi-schema-to-json-schema-3.1.1.tgz#e43b09680e652bf1b9e135db3f8648e979b76c07"
+  integrity sha512-FMvdhv9Jr9tULjJAQaQzhCmNYYj2vQFVnl7CGlLAImZvJal71oedXMGszpPaZTLftAk5TCHqjnirig+P6LZxug==
+  dependencies:
+    fast-deep-equal "^3.1.3"
+
+"@redocly/ajv@^8.6.4":
+  version "8.6.4"
+  resolved "https://registry.yarnpkg.com/@redocly/ajv/-/ajv-8.6.4.tgz#94053e7a9d4146d1a4feacd3813892873f229a85"
+  integrity sha512-y9qNj0//tZtWB2jfXNK3BX18BSBp9zNR7KE7lMysVHwbZtY392OJCjm6Rb/h4UHH2r1AqjNEHFD6bRn+DqU9Mw==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    json-schema-traverse "^1.0.0"
+    require-from-string "^2.0.2"
+    uri-js "^4.2.2"
+
+"@redocly/openapi-cli@^1.0.0-beta.54":
+  version "1.0.0-beta.94"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-cli/-/openapi-cli-1.0.0-beta.94.tgz#8bf84589864da941445bb21bd230e2c23c03c781"
+  integrity sha512-VHPVIP4K+KgYLDbQXIONS6GRMLsYz7tKa3QVVk83KS7X58fFC/N48hB1Ap2vnryj8HLSrG0yP9y6ZGH1t7kv7g==
+  dependencies:
+    "@redocly/openapi-core" "1.0.0-beta.94"
+    "@types/node" "^14.11.8"
+    assert-node-version "^1.0.3"
+    chokidar "^3.5.1"
+    colorette "^1.2.0"
+    glob "^7.1.6"
+    glob-promise "^3.4.0"
+    handlebars "^4.7.6"
+    portfinder "^1.0.26"
+    simple-websocket "^9.0.0"
+    yargs "17.0.1"
+
+"@redocly/openapi-core@1.0.0-beta.94":
+  version "1.0.0-beta.94"
+  resolved "https://registry.yarnpkg.com/@redocly/openapi-core/-/openapi-core-1.0.0-beta.94.tgz#7fc3a34aea8b0ee12ae2de26bedf90a4cf717366"
+  integrity sha512-xTklcobv+51bQVkUOpUiNY0GztL+0u3yGsy2BtldaHpcnNGMu3lu/utsoOHkiNTpgVEGyEWVZzBtF6Sz5v/Fkg==
+  dependencies:
+    "@redocly/ajv" "^8.6.4"
+    "@types/node" "^14.11.8"
+    colorette "^1.2.0"
+    js-levenshtein "^1.1.6"
+    js-yaml "^4.1.0"
+    lodash.isequal "^4.5.0"
+    minimatch "^3.0.4"
+    node-fetch "^2.6.1"
+    pluralize "^8.0.0"
+    yaml-ast-parser "0.0.43"
+
+"@stoplight/json-ref-resolver@^3.1.2":
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/@stoplight/json-ref-resolver/-/json-ref-resolver-3.1.3.tgz#243bc8f6b8f7a5f8b141e2296fd11fb634bfc3ff"
+  integrity sha512-SgoKXwVnlpIZUyAFX4W79eeuTWvXmNlMfICZixL16GZXnkjcW+uZnfmAU0ZIjcnaTgaI4mjfxn8LAP2KR6Cr0A==
+  dependencies:
+    "@stoplight/json" "^3.17.0"
+    "@stoplight/path" "^1.3.2"
+    "@stoplight/types" "^12.3.0"
+    "@types/urijs" "^1.19.16"
+    dependency-graph "~0.11.0"
+    fast-memoize "^2.5.2"
+    immer "^9.0.6"
+    lodash.get "^4.4.2"
+    lodash.set "^4.3.2"
+    tslib "^2.3.1"
+    urijs "^1.19.6"
+
+"@stoplight/json@^3.17.0":
+  version "3.18.1"
+  resolved "https://registry.yarnpkg.com/@stoplight/json/-/json-3.18.1.tgz#725b34b24e8b0c0f73113362be54c7a4f294cdba"
+  integrity sha512-QmELAqBS8DC+8YuG7+OvDVP6RaUVi8bzN0KKW2UEcZg+0a1sqeeZgfW079AmJIZg8HEN7udAt4iozIB8Dm0t1Q==
+  dependencies:
+    "@stoplight/ordered-object-literal" "^1.0.2"
+    "@stoplight/types" "^13.0.0"
+    jsonc-parser "~2.2.1"
+    lodash "^4.17.21"
+    safe-stable-stringify "^1.1"
+
+"@stoplight/ordered-object-literal@^1.0.2":
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/ordered-object-literal/-/ordered-object-literal-1.0.2.tgz#2a88a5ebc8b68b54837ac9a9ae7b779cdd862062"
+  integrity sha512-0ZMS/9sNU3kVo/6RF3eAv7MK9DY8WLjiVJB/tVyfF2lhr2R4kqh534jZ0PlrFB9CRXrdndzn1DbX6ihKZXft2w==
+
+"@stoplight/path@^1.3.2":
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/@stoplight/path/-/path-1.3.2.tgz#96e591496b72fde0f0cdae01a61d64f065bd9ede"
+  integrity sha512-lyIc6JUlUA8Ve5ELywPC8I2Sdnh1zc1zmbYgVarhXIp9YeAB0ReeqmGEOWNtlHkbP2DAA1AL65Wfn2ncjK/jtQ==
+
+"@stoplight/types@^12.3.0":
+  version "12.5.0"
+  resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-12.5.0.tgz#ebbeeb8c874de30e4cd9a1a2a6c8d6062c155da0"
+  integrity sha512-dwqYcDrGmEyUv5TWrDam5TGOxU72ufyQ7hnOIIDdmW5ezOwZaBFoR5XQ9AsH49w7wgvOqB2Bmo799pJPWnpCbg==
+  dependencies:
+    "@types/json-schema" "^7.0.4"
+    utility-types "^3.10.0"
+
+"@stoplight/types@^13.0.0":
+  version "13.0.0"
+  resolved "https://registry.yarnpkg.com/@stoplight/types/-/types-13.0.0.tgz#69a22d57d3a67f623a01929b10e25401c9635154"
+  integrity sha512-9OTVMiSUz2NlEW14OL6NKOuMTj3dtVVsugRwe3qbq0QnUpx/VLxOuO83n47rXZUTHvk69arOlFrDmRyZMw2DUg==
+  dependencies:
+    "@types/json-schema" "^7.0.4"
+    utility-types "^3.10.0"
+
+"@types/glob@*":
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
+  integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/json-schema@^7.0.4":
+  version "7.0.11"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
+  integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
+
+"@types/minimatch@*":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
+"@types/node@*":
+  version "17.0.25"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"
+  integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
+
+"@types/node@^14.11.8":
+  version "14.18.13"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.13.tgz#6ad4d9db59e6b3faf98dcfe4ca9d2aec84443277"
+  integrity sha512-Z6/KzgyWOga3pJNS42A+zayjhPbf2zM3hegRQaOPnLOzEi86VV++6FLDWgR1LGrVCRufP/ph2daa3tEa5br1zA==
+
+"@types/urijs@^1.19.16":
+  version "1.19.19"
+  resolved "https://registry.yarnpkg.com/@types/urijs/-/urijs-1.19.19.tgz#2789369799907fc11e2bc6e3a00f6478c2281b95"
+  integrity sha512-FDJNkyhmKLw7uEvTxx5tSXfPeQpO0iy73Ry+PmYZJvQy0QIWX8a7kJ4kLWRf+EbTPJEPDSgPXHaM7pzr5lmvCg==
+
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+argparse@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
+  integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+assert-node-version@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/assert-node-version/-/assert-node-version-1.0.3.tgz#caea5d1b6a58dbce59661208df1e1b9e4c580f91"
+  integrity sha1-yupdG2pY285ZZhII3x4bnkxYD5E=
+  dependencies:
+    expected-node-version "^1.0.0"
+    semver "^5.0.3"
+
+async@^2.6.2:
+  version "2.6.4"
+  resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+  integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
+  dependencies:
+    lodash "^4.17.14"
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+chokidar@^3.5.1:
+  version "3.5.3"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
+  integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colorette@^1.2.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40"
+  integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+debug@^3.1.1:
+  version "3.2.7"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
+  integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
+  dependencies:
+    ms "^2.1.1"
+
+debug@^4.3.1:
+  version "4.3.4"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+  integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+  dependencies:
+    ms "2.1.2"
+
+decamelize@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9"
+  integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==
+
+dependency-graph@~0.11.0:
+  version "0.11.0"
+  resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.11.0.tgz#ac0ce7ed68a54da22165a85e97a01d53f5eb2e27"
+  integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==
+
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+expected-node-version@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/expected-node-version/-/expected-node-version-1.0.2.tgz#b8d225b9bf676a9e87e06dbd615b52fc9d1e386b"
+  integrity sha1-uNIlub9nap6H4G29YVtS/J0eOGs=
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-memoize@^2.5.2:
+  version "2.5.2"
+  resolved "https://registry.yarnpkg.com/fast-memoize/-/fast-memoize-2.5.2.tgz#79e3bb6a4ec867ea40ba0e7146816f6cdce9b57e"
+  integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+get-caller-file@^2.0.5:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-promise@^3.4.0:
+  version "3.4.0"
+  resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
+  integrity sha512-q08RJ6O+eJn+dVanerAndJwIcumgbDdYiUT7zFQl3Wm1xD6fBKtah7H8ZJChj4wP+8C+QfeVy8xautR7rdmKEw==
+  dependencies:
+    "@types/glob" "*"
+
+glob@^7.1.6:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
+  integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+handlebars@^4.7.6:
+  version "4.7.7"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
+  integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
+  dependencies:
+    minimist "^1.2.5"
+    neo-async "^2.6.0"
+    source-map "^0.6.1"
+    wordwrap "^1.0.0"
+  optionalDependencies:
+    uglify-js "^3.1.4"
+
+immer@^9.0.6:
+  version "9.0.12"
+  resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.12.tgz#2d33ddf3ee1d247deab9d707ca472c8c942a0f20"
+  integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2, inherits@^2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
+  integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+js-levenshtein@^1.1.6:
+  version "1.1.6"
+  resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+  integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
+js-yaml@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602"
+  integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+  dependencies:
+    argparse "^2.0.1"
+
+json-schema-traverse@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
+  integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
+
+jsonc-parser@~2.2.1:
+  version "2.2.1"
+  resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.2.1.tgz#db73cd59d78cce28723199466b2a03d1be1df2bc"
+  integrity sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==
+
+lodash.get@^4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+  integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
+
+lodash.isequal@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
+  integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
+
+lodash.set@^4.3.2:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
+  integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
+
+lodash@^4.17.14, lodash@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+  integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+minimatch@^3.0.4:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
+  integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+minimist@^1.2.5, minimist@^1.2.6:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+  integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+mkdirp@^0.5.5:
+  version "0.5.6"
+  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
+  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==
+  dependencies:
+    minimist "^1.2.6"
+
+ms@2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
+  integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+
+ms@^2.1.1:
+  version "2.1.3"
+  resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
+  integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+neo-async@^2.6.0:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-fetch@^2.6.1:
+  version "2.6.7"
+  resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+  integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+  dependencies:
+    whatwg-url "^5.0.0"
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+picomatch@^2.0.4, picomatch@^2.2.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
+  integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+pluralize@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
+  integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
+
+portfinder@^1.0.26:
+  version "1.0.28"
+  resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
+  integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
+  dependencies:
+    async "^2.6.2"
+    debug "^3.1.1"
+    mkdirp "^0.5.5"
+
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+queue-microtask@^1.2.2:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
+  integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
+
+require-from-string@^2.0.2:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
+  integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
+
+safe-buffer@^5.1.0, safe-buffer@~5.2.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+safe-stable-stringify@^1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a"
+  integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==
+
+semver@^5.0.3:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
+  integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
+
+simple-websocket@^9.0.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/simple-websocket/-/simple-websocket-9.1.0.tgz#91cbb39eafefbe7e66979da6c639109352786a7f"
+  integrity sha512-8MJPnjRN6A8UCp1I+H/dSFyjwJhp6wta4hsVRhjf8w9qBHRzxYt14RaOcjvQnhD1N4yKOddEjflwMnQM4VtXjQ==
+  dependencies:
+    debug "^4.3.1"
+    queue-microtask "^1.2.2"
+    randombytes "^2.1.0"
+    readable-stream "^3.6.0"
+    ws "^7.4.2"
+
+source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+tr46@~0.0.3:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
+  integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
+
+tslib@^2.3.1:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
+  integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+
+uglify-js@^3.1.4:
+  version "3.15.4"
+  resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.15.4.tgz#fa95c257e88f85614915b906204b9623d4fa340d"
+  integrity sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+urijs@^1.19.6:
+  version "1.19.11"
+  resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.11.tgz#204b0d6b605ae80bea54bea39280cdb7c9f923cc"
+  integrity sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==
+
+util-deprecate@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+utility-types@^3.10.0:
+  version "3.10.0"
+  resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
+  integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
+
+webidl-conversions@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
+  integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
+
+whatwg-url@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
+  integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
+  dependencies:
+    tr46 "~0.0.3"
+    webidl-conversions "^3.0.0"
+
+wordwrap@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+  integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
+
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+ws@^7.4.2:
+  version "7.5.7"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.7.tgz#9e0ac77ee50af70d58326ecff7e85eb3fa375e67"
+  integrity sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==
+
+y18n@^5.0.5:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
+  integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
+
+yaml-ast-parser@0.0.43:
+  version "0.0.43"
+  resolved "https://registry.yarnpkg.com/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz#e8a23e6fb4c38076ab92995c5dca33f3d3d7c9bb"
+  integrity sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==
+
+yargs-parser@^20.2.2:
+  version "20.2.9"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+  integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
+
+yargs-parser@^21.0.0:
+  version "21.0.1"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35"
+  integrity sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==
+
+yargs@17.0.1:
+  version "17.0.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.0.1.tgz#6a1ced4ed5ee0b388010ba9fd67af83b9362e0bb"
+  integrity sha512-xBBulfCc8Y6gLFcrPvtqKz9hz8SO0l1Ni8GgDekvBX2ro0HRQImDGnikfc33cgzcYUSncapnNcZDjVFIH3f6KQ==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
+yargs@^17.0.1:
+  version "17.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.4.1.tgz#ebe23284207bb75cee7c408c33e722bfb27b5284"
+  integrity sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.3"
+    y18n "^5.0.5"
+    yargs-parser "^21.0.0"
-- 
GitLab


From 7365c231710ca267cadfc6a2c801d8fa5b93de61 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 21 Apr 2022 11:43:06 -0400
Subject: [PATCH 06/11] Fix dockerfile to use modern settings

---
 src/main/docker/Dockerfile.jvm | 37 +++++++++++++++++++++++-----------
 1 file changed, 25 insertions(+), 12 deletions(-)

diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm
index 0457e0bf..5492e55c 100644
--- a/src/main/docker/Dockerfile.jvm
+++ b/src/main/docker/Dockerfile.jvm
@@ -14,21 +14,34 @@
 # docker run -i --rm -p 8080:8080 eclipsefdn/git-eca-rest-api-jvm
 #
 ###
-FROM fabric8/java-alpine-openjdk8-jre:1.6.5
-ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
-ENV AB_ENABLED=jmx_exporter
+FROM registry.access.redhat.com/ubi8/ubi-minimal:8.4 
 
-# Be prepared for running in OpenShift too
-RUN adduser -G root --no-create-home --disabled-password 1001 \
-  && chown -R 1001 /deployments \
-  && chmod -R "g+rwX" /deployments \
-  && chown -R 1001:root /deployments
+ARG JAVA_PACKAGE=java-11-openjdk-headless
+ARG RUN_JAVA_VERSION=1.3.8
+ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
+# Install java and the run-java script
+# Also set up permissions for user `1001`
+RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
+    && microdnf update \
+    && microdnf clean all \
+    && mkdir /deployments \
+    && chown 1001 /deployments \
+    && chmod "g+rwX" /deployments \
+    && chown 1001:root /deployments \
+    && curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
+    && chown 1001 /deployments/run-java.sh \
+    && chmod 540 /deployments/run-java.sh \
+    && echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/conf/security/java.security
 
-COPY target/lib/* /deployments/lib/
-COPY target/*-runner.jar /deployments/app.jar
-EXPOSE 8080
+# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
+ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
+# We make four distinct layers so if there are application changes the library layers can be re-used
+COPY --chown=1001 target/quarkus-app/lib/ /deployments/lib/
+COPY --chown=1001 target/quarkus-app/*.jar /deployments/
+COPY --chown=1001 target/quarkus-app/app/ /deployments/app/
+COPY --chown=1001 target/quarkus-app/quarkus/ /deployments/quarkus/
 
-# run with user 1001
+EXPOSE 8090
 USER 1001
 
 ENTRYPOINT [ "/deployments/run-java.sh" ]
\ No newline at end of file
-- 
GitLab


From af1c870b69ea1925cd266868bc20d209caded19b Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Thu, 21 Apr 2022 13:42:58 -0400
Subject: [PATCH 07/11] Fix dockerignore to allow for updated packaging output

---
 .dockerignore | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.dockerignore b/.dockerignore
index b86c7ac3..94810d00 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,4 +1,5 @@
 *
 !target/*-runner
 !target/*-runner.jar
-!target/lib/*
\ No newline at end of file
+!target/lib/*
+!target/quarkus-app/*
\ No newline at end of file
-- 
GitLab


From 79ba36dd64e2d2c16e05f0f5c1c6cf80f5c69fbc Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 22 Apr 2022 13:40:31 -0400
Subject: [PATCH 08/11] Add user caching service, add previous oauth back in
 for performance

Built-in oauth wasn't properly caching auth results, so performance
tanked from having to fetch a fresh auth token on every request.
---
 docker-compose.yaml                           |  65 ++++++--
 pom.xml                                       |  10 +-
 .../git/eca/api/AccountsAPI.java              |  57 +++----
 .../git/eca/oauth/EclipseApi.java             |  47 ++++++
 .../git/eca/resource/ValidationResource.java  |  98 +-----------
 .../git/eca/service/OAuthService.java         |  28 ++++
 .../git/eca/service/UserService.java          |  10 ++
 .../eca/service/impl/CachedUserService.java   | 141 ++++++++++++++++++
 .../eca/service/impl/DefaultOAuthService.java |  90 +++++++++++
 src/main/k8s/staging.yml                      |   2 +-
 src/main/resources/application.properties     |  21 +--
 11 files changed, 408 insertions(+), 161 deletions(-)
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java

diff --git a/docker-compose.yaml b/docker-compose.yaml
index 5ff2a86e..81dd6f83 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,16 +1,49 @@
-web:
-  image: 'gitlab/gitlab-ce:latest'
-  restart: always
-  environment:
-    VIRTUAL_HOST: "gitlab.dev.docker"
-    VIRTUAL_PORT: 443
-    VIRTUAL_PROTO: https
-    CERT_NAME: dev.docker
-  ports:
-    - 443
-    - 80
-    - 22
-  volumes:
-    - '/localdocker/gitlab/config:/etc/gitlab'
-    - '/localdocker/gitlab/logs:/var/log/gitlab'
-    - '/localdocker/gitlab/data:/var/opt/gitlab'
+version: '3'
+services:
+  web:
+    container_name: gitlab
+    image: 'gitlab/gitlab-ce:latest'
+    restart: always
+    environment:
+      VIRTUAL_HOST: "gitlab.dev.docker"
+      VIRTUAL_PORT: 443
+      VIRTUAL_PROTO: https
+      CERT_NAME: dev.docker
+    ports:
+      - 443:443
+      - 80:80
+      - 22:22
+    volumes:
+      - '/localdocker/gitlab/config:/etc/gitlab'
+      - '/localdocker/gitlab/logs:/var/log/gitlab'
+      - '/localdocker/gitlab/data:/var/opt/gitlab'
+  postgres:
+    container_name: postgres
+    image: postgres:12.4
+    volumes:
+      - ./volumes/postgres:/var/lib/postgresql/data
+    environment:
+      - POSTGRES_DB=${GIT_ECA_POSTGRES_DB}
+      - POSTGRES_USER=${GIT_ECA_POSTGRES_USER}
+      - POSTGRES_PASSWORD=${GIT_ECA_POSTGRES_PASSWORD}
+    ports:
+      - 5432
+  keycloak:
+    container_name: keycloak
+    image: jboss/keycloak:11.0.1
+    environment:
+      - VIRTUAL_HOST=keycloak
+      - VIRTUAL_PORT=8080
+      - DB_VENDOR=POSTGRES
+      - DB_DATABASE=${GIT_ECA_POSTGRES_DB}
+      - DB_SCHEMA=public
+      - DB_ADDR=postgres
+      - DB_PORT=5432
+      - DB_USER=${GIT_ECA_POSTGRES_USER}
+      - DB_PASSWORD=${GIT_ECA_POSTGRES_PASSWORD}
+      - KEYCLOAK_USER=${GIT_ECA_KEYCLOAK_USER}
+      - KEYCLOAK_PASSWORD=${GIT_ECA_KEYCLOAK_PASSWORD}
+    ports:
+      - '8080:8080'
+    depends_on:
+      - postgres
diff --git a/pom.xml b/pom.xml
index 3a9eed7b..20561aad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 	<artifactId>git-eca</artifactId>
 	<version>0.0.1</version>
 	<properties>
-		<eclipse-api-version>0.6-SNAPSHOT</eclipse-api-version>
+		<eclipse-api-version>0.6.3-SNAPSHOT</eclipse-api-version>
 		<compiler-plugin.version>3.8.1</compiler-plugin.version>
 		<maven.compiler.parameters>true</maven.compiler.parameters>
 		<maven.compiler.source>11</maven.compiler.source>
@@ -59,14 +59,6 @@
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-smallrye-context-propagation</artifactId>
 		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-oidc</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>io.quarkus</groupId>
-			<artifactId>quarkus-oidc-client-filter</artifactId>
-		</dependency>
 		<dependency>
 			<groupId>io.quarkus</groupId>
 			<artifactId>quarkus-rest-client</artifactId>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
index 1aadfe55..5096632e 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
@@ -12,6 +12,7 @@ package org.eclipsefoundation.git.eca.api;
 import java.util.List;
 
 import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
@@ -20,47 +21,37 @@ import javax.ws.rs.QueryParam;
 import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
 import org.eclipsefoundation.git.eca.model.EclipseUser;
 
-import io.quarkus.oidc.client.filter.OidcClientFilter;
-import io.quarkus.security.Authenticated;
-
 /**
- * Binding interface for the Eclipse Foundation user account API. Runtime
- * implementations are automatically generated by Quarkus at compile time. As
- * the API deals with sensitive information, authentication is required to
- * access this endpoint.
+ * Binding interface for the Eclipse Foundation user account API. Runtime implementations are automatically generated by
+ * Quarkus at compile time. As the API deals with sensitive information, authentication is required to access this
+ * endpoint.
  * 
  * @author Martin Lowe
  *
  */
-@OidcClientFilter
-@Authenticated
 @RegisterRestClient
 @Produces("application/json")
 public interface AccountsAPI {
 
-	/**
-	 * Retrieves all user objects that match the given query parameters.
-	 * 
-	 * @param id   user ID of the Eclipse account to retrieve
-	 * @param name the given name to match against for Eclipse accounts
-	 * @param mail the email address to match against for Eclipse accounts
-	 * @return all matching eclipse accounts
-	 */
-	@GET
-	@Path("/account/profile")
-	List<EclipseUser> getUsers(@QueryParam("uid") String id,
-			@QueryParam("name") String name, @QueryParam("mail") String mail);
-
-	/**
-	 * Retrieves user objects that matches the given Github username.
-	 * 
-	 * @param authBearer authorization header value for validating call
-	 * @param uname      username of the Github account to retrieve Eclipse Account
-	 *                   for
-	 * @return the matching Eclipse account or null
-	 */
-	@GET
-	@Path("/github/profile/{uname}")
-	EclipseUser getUserByGithubUname(@PathParam("uname") String uname);
+    /**
+     * Retrieves all user objects that use the given mail address.
+     * 
+     * @param mail the email address to match against for Eclipse accounts
+     * @return all matching eclipse accounts
+     */
+    @GET
+    @Path("/account/profile")
+    List<EclipseUser> getUsers(@HeaderParam("Authorization") String token, @QueryParam("mail") String mail);
+
+    /**
+     * Retrieves user objects that matches the given Github username.
+     * 
+     * @param authBearer authorization header value for validating call
+     * @param uname username of the Github account to retrieve Eclipse Account for
+     * @return the matching Eclipse account or null
+     */
+    @GET
+    @Path("/github/profile/{uname}")
+    EclipseUser getUserByGithubUname(@HeaderParam("Authorization") String token, @PathParam("uname") String uname);
 
 }
diff --git a/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java b/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
new file mode 100644
index 00000000..226caf4e
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
@@ -0,0 +1,47 @@
+/*******************************************************************************
+ * Copyright (C) 2020 Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ * 
+ * SPDX-License-Identifier: EPL-2.0
+ ******************************************************************************/
+package org.eclipsefoundation.git.eca.oauth;
+
+import com.github.scribejava.core.builder.api.DefaultApi20;
+import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
+import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme;
+
+/**
+ * Wrapper around the OAuth API for Scribejava. Enables OAuth2.0 binding to the
+ * Eclipse Foundation OAuth server.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public class EclipseApi extends DefaultApi20 {
+
+    @Override
+    public String getAccessTokenEndpoint() {
+        return "https://accounts.eclipse.org/oauth2/token";
+    }
+
+    @Override
+    protected String getAuthorizationBaseUrl() {
+        return null;
+    }
+
+    @Override
+    public ClientAuthentication getClientAuthentication() {
+        return RequestBodyAuthenticationScheme.instance();
+    }
+
+    private static class InstanceHolder {
+        private static final EclipseApi INSTANCE = new EclipseApi();
+    }
+
+    public static EclipseApi instance() {
+        return InstanceHolder.INSTANCE;
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
index 8bae1e5c..2a14d535 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -15,10 +15,8 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Optional;
-import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
-import javax.annotation.PostConstruct;
 import javax.inject.Inject;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.POST;
@@ -31,7 +29,6 @@ import javax.ws.rs.core.Response;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.service.CachingService;
-import org.eclipsefoundation.git.eca.api.AccountsAPI;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
@@ -43,6 +40,7 @@ import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
 import org.eclipsefoundation.git.eca.service.ProjectsService;
+import org.eclipsefoundation.git.eca.service.UserService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -69,14 +67,10 @@ public class ValidationResource {
   @Inject
   @ConfigProperty(name = "eclipse.mail.allowlist")
   List<String> allowListUsers;
-  @Inject
-  @ConfigProperty(name = "eclipse.noreply.email-patterns")
-  List<String> emailPatterns;
 
   // eclipse API rest client interfaces
   @Inject
-  @RestClient
-  AccountsAPI accounts;
+  UserService users;
   @Inject
   @RestClient
   BotsAPI bots;
@@ -87,15 +81,6 @@ public class ValidationResource {
   @Inject
   ProjectsService projects;
 
-  // rendered list of regex values
-  List<Pattern> patterns;
-
-  @PostConstruct
-  void init() {
-    // compile the patterns once per object to save processing time
-    this.patterns = emailPatterns.stream().map(Pattern::compile).collect(Collectors.toList());
-  }
-
   /**
    * Consuming a JSON request, this method will validate all passed commits, using
    * the repo URL and
@@ -514,12 +499,11 @@ public class ValidationResource {
     // get the Eclipse account for the user
     try {
       // use cache to avoid asking for the same user repeatedly on repeated requests
-      Optional<EclipseUser> foundUser = cache.get("user|" + user.getMail(), new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(user));
-      if (!foundUser.isPresent()) {
+      EclipseUser foundUser = users.getUser(user.getMail());
+      if (foundUser == null) {
         LOGGER.warn("No users found for mail '{}'", user.getMail());
-        return null;
       }
-      return foundUser.get();
+      return foundUser;
     } catch (WebApplicationException e) {
       Response r = e.getResponse();
       if (r != null && r.getStatus() == 404) {
@@ -531,78 +515,6 @@ public class ValidationResource {
     return null;
   }
 
-  /**
-   * Checks for standard and noreply email address matches for a Git user and
-   * converts to a
-   * Eclipse Foundation account object.
-   * 
-   * @param user the user to attempt account retrieval for.
-   * @return the user account if found by mail, or null if none found.
-   */
-  private EclipseUser retrieveUser(GitUser user) {
-    // check for noreply (no reply will never have user account, and fails fast)
-    EclipseUser eclipseUser = checkForNoReplyUser(user);
-    if (eclipseUser != null) {
-      return eclipseUser;
-    }
-    // standard user check (returns best match)
-    LOGGER.debug("Checking user with mail {}", user.getMail());
-    try {
-      List<EclipseUser> users = accounts.getUsers(null, null, user.getMail());
-      if (users != null && !users.isEmpty()) {
-        return users.get(0);
-      }
-    } catch (WebApplicationException e) {
-      LOGGER.warn("Could not find user account with mail '{}'", user.getMail());
-    }
-    return null;
-  }
-
-  /**
-   * Checks git user for no-reply address, and attempts to ratify user through
-   * reverse lookup in API service.
-   * Currently, this service only recognizes Github no-reply addresses as they
-   * have a route to be mapped.
-   * 
-   * @param user the Git user account to check for no-reply mail address
-   * @return the Eclipse user if email address is detected no reply and one can be
-   *         mapped, otherwise null
-   */
-  private EclipseUser checkForNoReplyUser(GitUser user) {
-    LOGGER.debug("Checking user with mail {} for no-reply", user.getMail());
-    boolean isNoReply = patterns.stream().anyMatch(pattern -> pattern.matcher(user.getMail().trim()).find());
-    if (isNoReply) {
-      // get the username/ID string before the first @ symbol.
-      String noReplyUser = user.getMail().substring(0, user.getMail().indexOf("@", 0));
-      // split based on +, if more than one part, use second (contains user),
-      // otherwise, use whole string
-      String[] nameParts = noReplyUser.split("[\\+]");
-      String namePart;
-      if (nameParts.length > 1 && nameParts[1] != null) {
-        namePart = nameParts[1];
-      } else {
-        namePart = nameParts[0];
-      }
-      String uname = namePart.trim();
-      LOGGER.debug("User with mail {} detected as noreply account, checking services for username match on '{}'",
-          user.getMail(), uname);
-
-      // check github for no-reply (only allowed noreply currently)
-      if (user.getMail().endsWith("noreply.github.com")) {
-        try {
-          // check for Github no reply, return if set
-          EclipseUser eclipseUser = accounts.getUserByGithubUname(uname);
-          if (eclipseUser != null) {
-            return eclipseUser;
-          }
-        } catch (WebApplicationException e) {
-          LOGGER.warn("No match for '{}' in Github", uname);
-        }
-      }
-    }
-    return null;
-  }
-
   private List<JsonNode> getBots() {
     Optional<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class,  () -> bots.getBots());
     if (!allBots.isPresent()) {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
new file mode 100644
index 00000000..eaa35324
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * Copyright (C) 2020 Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ * 
+ * SPDX-License-Identifier: EPL-2.0
+ ******************************************************************************/
+package org.eclipsefoundation.git.eca.service;
+
+/**
+ * Used to generate OAuth tokens for use with internal services rather than bolted on introspection. This is required
+ * over the (now deprecated) Elytron plugin or the OIDC plugin as those plugins work with requests to validate incoming
+ * rather than outgoing requests.
+ * 
+ * @author Martin Lowe
+ *
+ */
+public interface OAuthService {
+
+    /**
+     * Retrieve an access token for the service from the Eclipse API for internal usage.
+     * 
+     * @return current access token, or null if none could be retrieved for current API credentials/settings.
+     */
+    String getToken();
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
new file mode 100644
index 00000000..377b9a0d
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
@@ -0,0 +1,10 @@
+package org.eclipsefoundation.git.eca.service;
+
+import org.eclipsefoundation.git.eca.model.EclipseUser;
+
+public interface UserService {
+
+    EclipseUser getUser(String mail);
+
+    EclipseUser getUserByGithubUsername(String username);
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
new file mode 100644
index 00000000..a86db763
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -0,0 +1,141 @@
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.ws.rs.WebApplicationException;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.git.eca.api.AccountsAPI;
+import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.service.OAuthService;
+import org.eclipsefoundation.git.eca.service.UserService;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Wrapped cached and authenticated access to user objects.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Singleton
+public class CachedUserService implements UserService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(CachedUserService.class);
+
+    @Inject
+    @ConfigProperty(name = "eclipse.noreply.email-patterns")
+    List<String> emailPatterns;
+
+    // eclipse API rest client interfaces
+    @Inject
+    @RestClient
+    AccountsAPI accounts;
+
+    @Inject
+    OAuthService oauth;
+    @Inject
+    CachingService cache;
+
+    // rendered list of regex values
+    List<Pattern> patterns;
+
+    @PostConstruct
+    void init() {
+        // compile the patterns once per object to save processing time
+        this.patterns = emailPatterns.stream().map(Pattern::compile).collect(Collectors.toList());
+    }
+
+    @Override
+    public EclipseUser getUser(String mail) {
+        return cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(mail))
+                .orElseGet(() -> null);
+    }
+
+    @Override
+    public EclipseUser getUserByGithubUsername(String username) {
+        return cache.get("gh:" + username, new MultivaluedMapImpl<>(), EclipseUser.class,
+                () -> accounts.getUserByGithubUname(getBearerToken(), username)).orElseGet(() -> null);
+    }
+
+    /**
+     * Checks for standard and noreply email address matches for a Git user and converts to a Eclipse Foundation account
+     * object.
+     * 
+     * @param user the user to attempt account retrieval for.
+     * @return the user account if found by mail, or null if none found.
+     */
+    private EclipseUser retrieveUser(String mail) {
+        LOGGER.error("Getting fresh user for {}", mail);
+        // check for noreply (no reply will never have user account, and fails fast)
+        EclipseUser eclipseUser = checkForNoReplyUser(mail);
+        if (eclipseUser != null) {
+            return eclipseUser;
+        }
+        // standard user check (returns best match)
+        LOGGER.debug("Checking user with mail {}", mail);
+        try {
+            List<EclipseUser> matches = cache
+                    .get(mail, null, EclipseUser.class, () -> accounts.getUsers(getBearerToken(), mail))
+                    .orElseGet(Collections::emptyList);
+            if (!matches.isEmpty()) {
+                return matches.get(0);
+            }
+        } catch (WebApplicationException e) {
+            LOGGER.warn("Could not find user account with mail '{}'", mail);
+        }
+        return null;
+    }
+
+    /**
+     * Checks git user for no-reply address, and attempts to ratify user through reverse lookup in API service.
+     * Currently, this service only recognizes Github no-reply addresses as they have a route to be mapped.
+     * 
+     * @param user the Git user account to check for no-reply mail address
+     * @return the Eclipse user if email address is detected no reply and one can be mapped, otherwise null
+     */
+    private EclipseUser checkForNoReplyUser(String mail) {
+        LOGGER.debug("Checking user with mail {} for no-reply", mail);
+        boolean isNoReply = patterns.stream().anyMatch(pattern -> pattern.matcher(mail.trim()).find());
+        if (isNoReply) {
+            // get the username/ID string before the first @ symbol.
+            String noReplyUser = mail.substring(0, mail.indexOf("@", 0));
+            // split based on +, if more than one part, use second (contains user),
+            // otherwise, use whole string
+            String[] nameParts = noReplyUser.split("[\\+]");
+            String namePart;
+            if (nameParts.length > 1 && nameParts[1] != null) {
+                namePart = nameParts[1];
+            } else {
+                namePart = nameParts[0];
+            }
+            String uname = namePart.trim();
+            LOGGER.debug("User with mail {} detected as noreply account, checking services for username match on '{}'",
+                    mail, uname);
+
+            // check github for no-reply (only allowed noreply currently)
+            if (mail.endsWith("noreply.github.com")) {
+                try {
+                    // check for Github no reply, return if set
+                    return getUserByGithubUsername(uname);
+                } catch (WebApplicationException e) {
+                    LOGGER.warn("No match for '{}' in Github", uname);
+                }
+            }
+        }
+        return null;
+    }
+
+    private String getBearerToken() {
+        return "Bearer " + oauth.getToken();
+    }
+
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
new file mode 100644
index 00000000..9c8198ce
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * Copyright (C) 2020 Eclipse Foundation
+ * 
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ * 
+ * SPDX-License-Identifier: EPL-2.0
+ ******************************************************************************/
+package org.eclipsefoundation.git.eca.service.impl;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Singleton;
+
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipsefoundation.git.eca.oauth.EclipseApi;
+import org.eclipsefoundation.git.eca.service.OAuthService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.oauth.OAuth20Service;
+
+/**
+ * Default implementation for requesting an OAuth request token. The reason that this class is implemented over the
+ * other implementations baked into Quarkus is to better bind to the Drupal OAuth APIs.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@Singleton
+public class DefaultOAuthService implements OAuthService {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuthService.class);
+
+    @ConfigProperty(name = "oauth2.client-id")
+    String id;
+    @ConfigProperty(name = "oauth2.client-secret")
+    String secret;
+    @ConfigProperty(name = "oauth2.scope")
+    String scope;
+
+    // service reference (as we only need one)
+    private OAuth20Service service;
+
+    // token state vars
+    private long expirationTime;
+    private String accessToken;
+
+    /**
+     * Create an OAuth service reference.
+     */
+    @PostConstruct
+    void createServiceRef() {
+        this.service = new ServiceBuilder(id).apiSecret(secret).scope(scope).build(EclipseApi.instance());
+    }
+
+    @Override
+    public String getToken() {
+        // lock on the class instance to stop multiple threads from requesting new
+        // tokens at the same time
+        synchronized (this) {
+            if (accessToken == null || System.currentTimeMillis() >= expirationTime) {
+                // clear access token
+                this.accessToken = null;
+                try {
+                    OAuth2AccessToken requestToken = service.getAccessTokenClientCredentialsGrant();
+                    if (requestToken != null) {
+                        this.accessToken = requestToken.getAccessToken();
+                        this.expirationTime = System.currentTimeMillis()
+                                + TimeUnit.SECONDS.toMillis(requestToken.getExpiresIn().longValue());
+                    }
+                } catch (IOException e) {
+                    LOGGER.error("Issue communicating with OAuth server for authentication", e);
+                } catch (InterruptedException e) {
+                    LOGGER.error("Authentication communication was interrupted before completion", e);
+                    Thread.currentThread().interrupt();
+                } catch (ExecutionException e) {
+                    LOGGER.error("Error while retrieving access token for request", e);
+                }
+            }
+        }
+        return accessToken;
+    }
+
+}
diff --git a/src/main/k8s/staging.yml b/src/main/k8s/staging.yml
index 1109179e..7bbee7a9 100644
--- a/src/main/k8s/staging.yml
+++ b/src/main/k8s/staging.yml
@@ -51,7 +51,7 @@ spec:
       volumes:
       - name: api-oauth-token
         secret:
-          secretName: git-eca-rest-api
+          secretName: git-eca-rest-api-staging
 ---
 apiVersion: "v1"
 kind: "Service"
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index f41f5d75..16edcbc4 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -6,17 +6,20 @@ eclipse.noreply.email-patterns=@users.noreply.github.com\$
 
 ## Expect to be mounted to '/git' to match current URL spec
 quarkus.http.root-path=/git
-
-## OAUTH CONFIG
 quarkus.http.port=8080
 
-quarkus.keycloak.devservices.enabled=false
+## OAUTH CONFIG
 quarkus.oauth2.enabled=false
 quarkus.oidc.enabled=false
-quarkus.oidc-client.auth-server-url=https://accounts.eclipse.org/oauth2
-quarkus.oidc-client.discovery-enabled=false
-quarkus.oidc-client.token-path=/token
-quarkus.oidc-client.grant.type=client
-quarkus.oidc-client.scopes=eclipsefdn_view_all_profiles
+oauth2.scope=eclipsefdn_view_all_profiles
+oauth2.client-id=placeholder
+oauth2.client-secret=placeholder
+
+quarkus.cache.caffeine."default".initial-capacity=1000
+quarkus.cache.caffeine."default".expire-after-write=1H
+quarkus.cache.caffeine."record".initial-capacity=${quarkus.cache.caffeine."default".initial-capacity}
+quarkus.cache.caffeine."record".expire-after-write=${quarkus.cache.caffeine."default".expire-after-write}
 
-eclipse.mail.allowlist=noreply@github.com
\ No newline at end of file
+eclipse.mail.allowlist=noreply@github.com
+#%dev.quarkus.log.level=DEBUG
+#quarkus.log.category."org.eclipsefoundation".level=DEBUG
\ No newline at end of file
-- 
GitLab


From dea89047c0189de95ba5cd81bb4a51baecf2ea0a Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 22 Apr 2022 13:58:01 -0400
Subject: [PATCH 09/11] Fix broken tests around auth + changed accounts api
 signatures

---
 .../git/eca/test/api/MockAccountsAPI.java      | 10 ++--------
 .../test/service/impl/MockOAuthService.java    | 18 ++++++++++++++++++
 2 files changed, 20 insertions(+), 8 deletions(-)
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java

diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
index 1a93328c..0bc5880a 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
@@ -54,15 +54,9 @@ public class MockAccountsAPI implements AccountsAPI {
     }
 
     @Override
-    public List<EclipseUser> getUsers(String id, String name, String mail) {
+    public List<EclipseUser> getUsers(String auth, String mail) {
         return src.stream().filter(user -> {
             boolean matches = true;
-            if (id != null && !Integer.toString(user.getUid()).equals(id)) {
-                matches = false;
-            }
-            if (name != null && !user.getName().equals(name)) {
-                matches = false;
-            }
             if (mail != null && !user.getMail().equalsIgnoreCase(mail)) {
                 matches = false;
             }
@@ -71,7 +65,7 @@ public class MockAccountsAPI implements AccountsAPI {
     }
 
     @Override
-    public EclipseUser getUserByGithubUname(String uname) {
+    public EclipseUser getUserByGithubUname(String auth, String uname) {
         // assume GH username == Eclipse uname for simplicity of test
         return src.stream().filter(user -> uname.equalsIgnoreCase(user.getName())).findFirst().orElse(null);
     }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java
new file mode 100644
index 00000000..21508dbf
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java
@@ -0,0 +1,18 @@
+package org.eclipsefoundation.git.eca.test.service.impl;
+
+import javax.inject.Singleton;
+
+import org.eclipsefoundation.git.eca.service.OAuthService;
+
+import io.quarkus.test.Mock;
+
+@Mock
+@Singleton
+public class MockOAuthService implements OAuthService {
+
+    @Override
+    public String getToken() {
+        return "";
+    }
+
+}
-- 
GitLab


From 380eea914b74b578a8953b740e736dea6fb68871 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 22 Apr 2022 15:44:48 -0400
Subject: [PATCH 10/11] Fix scopes and tests that were broken from switch to
 userservice

---
 .../git/eca/api/AccountsAPI.java              |  2 +
 .../git/eca/api/BotsAPI.java                  |  2 +
 .../git/eca/api/ProjectsAPI.java              |  2 +
 .../eca/service/impl/CachedUserService.java   |  4 +-
 .../eca/service/impl/DefaultOAuthService.java |  4 +-
 src/main/resources/application.properties     |  3 +-
 .../eca/resource/ValidationResourceTest.java  | 44 +++++++++++
 .../git/eca/test/api/MockAccountsAPI.java     | 73 -------------------
 .../test/service/impl/MockUserService.java    | 10 +++
 src/test/resources/application.properties     |  4 +-
 10 files changed, 68 insertions(+), 80 deletions(-)
 delete mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
 create mode 100644 src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java

diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
index 5096632e..171fa4fd 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/AccountsAPI.java
@@ -11,6 +11,7 @@ package org.eclipsefoundation.git.eca.api;
 
 import java.util.List;
 
+import javax.enterprise.context.ApplicationScoped;
 import javax.ws.rs.GET;
 import javax.ws.rs.HeaderParam;
 import javax.ws.rs.Path;
@@ -29,6 +30,7 @@ import org.eclipsefoundation.git.eca.model.EclipseUser;
  * @author Martin Lowe
  *
  */
+@ApplicationScoped
 @RegisterRestClient
 @Produces("application/json")
 public interface AccountsAPI {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/BotsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/BotsAPI.java
index 961277fb..b0f60b0d 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/BotsAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/BotsAPI.java
@@ -11,6 +11,7 @@ package org.eclipsefoundation.git.eca.api;
 
 import java.util.List;
 
+import javax.enterprise.context.ApplicationScoped;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -25,6 +26,7 @@ import com.fasterxml.jackson.databind.JsonNode;
  * @author Martin Lowe
  *
  */
+@ApplicationScoped
 @Path("/bots")
 @RegisterRestClient
 public interface BotsAPI {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
index 6c4db5ce..93a74582 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
@@ -11,6 +11,7 @@ package org.eclipsefoundation.git.eca.api;
 
 import java.util.List;
 
+import javax.enterprise.context.ApplicationScoped;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
@@ -26,6 +27,7 @@ import org.eclipsefoundation.git.eca.model.Project;
  * @author Martin Lowe
  *
  */
+@ApplicationScoped
 @Path("/api/projects")
 @RegisterRestClient
 public interface ProjectsAPI {
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
index a86db763..ba0d037a 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -6,8 +6,8 @@ import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 import javax.annotation.PostConstruct;
+import javax.enterprise.context.ApplicationScoped;
 import javax.inject.Inject;
-import javax.inject.Singleton;
 import javax.ws.rs.WebApplicationException;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
@@ -27,7 +27,7 @@ import org.slf4j.LoggerFactory;
  * @author Martin Lowe
  *
  */
-@Singleton
+@ApplicationScoped
 public class CachedUserService implements UserService {
     private static final Logger LOGGER = LoggerFactory.getLogger(CachedUserService.class);
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
index 9c8198ce..72047985 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
@@ -14,7 +14,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 
 import javax.annotation.PostConstruct;
-import javax.inject.Singleton;
+import javax.enterprise.context.ApplicationScoped;
 
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipsefoundation.git.eca.oauth.EclipseApi;
@@ -33,7 +33,7 @@ import com.github.scribejava.core.oauth.OAuth20Service;
  * @author Martin Lowe
  *
  */
-@Singleton
+@ApplicationScoped
 public class DefaultOAuthService implements OAuthService {
     private static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuthService.class);
 
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 16edcbc4..4ddc2d46 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,4 +1,5 @@
-org.eclipsefoundation.git.eca.api.AccountsAPI/mp-rest/url=https://api.eclipse.org
+quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".scope=javax.enterprise.context.ApplicationScoped
+quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".url=https://api.eclipse.org
 org.eclipsefoundation.git.eca.api.ProjectsAPI/mp-rest/url=https://projects.eclipse.org
 org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 
diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
index 886d1f6e..2d81011a 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ValidationResourceTest.java
@@ -23,19 +23,25 @@ import javax.inject.Inject;
 
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.model.Commit;
+import org.eclipsefoundation.git.eca.model.EclipseUser;
+import org.eclipsefoundation.git.eca.model.EclipseUser.ECA;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.ProviderType;
+import org.eclipsefoundation.git.eca.service.impl.CachedUserService;
 import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mockito;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.junit.mockito.InjectMock;
 import io.restassured.http.ContentType;
 
 /**
@@ -53,8 +59,46 @@ class ValidationResourceTest {
     @Inject
     ObjectMapper json;
 
+    @InjectMock
+    CachedUserService accounts;
+
     @BeforeEach
     void cacheClear() {
+        int id = 0;
+        // standard user fetches
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("newbie@important.co")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("newbie@important.co")
+                        .setName("newbieAnon").setECA(ECA.builder().build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("slom@eclipse-foundation.org")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
+                        .setMail("slom@eclipse-foundation.org").setName("barshall_blathers")
+                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("tester@eclipse-foundation.org")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
+                        .setMail("tester@eclipse-foundation.org").setName("mctesterson")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("code.wiz@important.co")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("code.wiz@important.co")
+                        .setName("da_wizz")
+                        .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("grunt@important.co")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
+                        .setName("grunter")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("paper.pusher@important.co")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(false).setUid(id++)
+                        .setMail("paper.pusher@important.co").setName("sumAnalyst")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+
+        // gh user fetches
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("grunter@users.noreply.github.com")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
+                        .setName("grunter")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
+        Mockito.when(accounts.getUser(ArgumentMatchers.eq("123456789+grunter@users.noreply.github.com")))
+                .thenReturn(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co")
+                        .setName("grunter")
+                        .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
         // if dev servers are run on the same machine, some values may live in the cache
         cs.removeAll();
     }
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
deleted file mode 100644
index 0bc5880a..00000000
--- a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockAccountsAPI.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*******************************************************************************
- * Copyright (C) 2020 Eclipse Foundation
- * 
- * This program and the accompanying materials are made
- * available under the terms of the Eclipse Public License 2.0
- * which is available at https://www.eclipse.org/legal/epl-2.0/
- * 
- * SPDX-License-Identifier: EPL-2.0
- ******************************************************************************/
-package org.eclipsefoundation.git.eca.test.api;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.annotation.PostConstruct;
-import javax.enterprise.context.ApplicationScoped;
-
-import org.eclipse.microprofile.rest.client.inject.RestClient;
-import org.eclipsefoundation.git.eca.api.AccountsAPI;
-import org.eclipsefoundation.git.eca.model.EclipseUser;
-import org.eclipsefoundation.git.eca.model.EclipseUser.ECA;
-
-import io.quarkus.test.Mock;
-
-@Mock
-@RestClient
-@ApplicationScoped
-public class MockAccountsAPI implements AccountsAPI {
-
-    private List<EclipseUser> src;
-
-    @PostConstruct
-    public void build() {
-        this.src = new ArrayList<>();
-        int id = 0;
-        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("newbie@important.co")
-                .setName("newbieAnon").setECA(ECA.builder().build()).build());
-        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("slom@eclipse-foundation.org")
-                .setName("barshall_blathers")
-                .setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()).build());
-        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("tester@eclipse-foundation.org")
-                .setName("mctesterson").setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build())
-                .build());
-        src.add(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("code.wiz@important.co")
-                .setName("da_wizz").setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build())
-                .build());
-        src.add(EclipseUser.builder().setIsCommitter(true).setUid(id++).setMail("grunt@important.co").setName("grunter")
-                .setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()).build());
-        src.add(EclipseUser.builder().setIsCommitter(false).setUid(id++).setMail("paper.pusher@important.co")
-                .setName("sumAnalyst").setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build())
-                .build());
-
-    }
-
-    @Override
-    public List<EclipseUser> getUsers(String auth, String mail) {
-        return src.stream().filter(user -> {
-            boolean matches = true;
-            if (mail != null && !user.getMail().equalsIgnoreCase(mail)) {
-                matches = false;
-            }
-            return matches;
-        }).collect(Collectors.toList());
-    }
-
-    @Override
-    public EclipseUser getUserByGithubUname(String auth, String uname) {
-        // assume GH username == Eclipse uname for simplicity of test
-        return src.stream().filter(user -> uname.equalsIgnoreCase(user.getName())).findFirst().orElse(null);
-    }
-
-}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java
new file mode 100644
index 00000000..c799f363
--- /dev/null
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockUserService.java
@@ -0,0 +1,10 @@
+package org.eclipsefoundation.git.eca.test.service.impl;
+
+import javax.enterprise.context.ApplicationScoped;
+
+import org.eclipsefoundation.git.eca.service.impl.CachedUserService;
+
+public class MockUserService extends CachedUserService {
+
+    
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index e1cee2f0..81d29deb 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,8 +1,9 @@
-org.eclipsefoundation.git.eca.api.AccountsAPI/mp-rest/url=https://api.eclipse.org
+
 org.eclipsefoundation.git.eca.api.ProjectsAPI/mp-rest/url=https://projects.eclipse.org
 org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 
 eclipse.noreply.email-patterns=@users.noreply.github.com\$
+eclipse.mail.allowlist=noreply@github.com
 
 ## Expect to be mounted to '/git' to match current URL spec
 quarkus.http.root-path=/git
@@ -14,5 +15,4 @@ quarkus.keycloak.devservices.enabled=false
 quarkus.oidc-client.enabled=false
 quarkus.http.port=8080
 
-eclipse.mail.allowlist=noreply@github.com
 #quarkus.log.level=DEBUG
\ No newline at end of file
-- 
GitLab


From 2b417ae91ba65f696d20b2e6dfe6bd043e844cef Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Mon, 25 Apr 2022 09:14:34 -0400
Subject: [PATCH 11/11] Fix issue w/ nested caching calls killing user service
 requests

---
 .../eca/service/impl/CachedUserService.java   | 27 ++++++++++++-------
 1 file changed, 18 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
index ba0d037a..8b8e523f 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -1,7 +1,7 @@
 package org.eclipsefoundation.git.eca.service.impl;
 
-import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -56,14 +56,25 @@ public class CachedUserService implements UserService {
 
     @Override
     public EclipseUser getUser(String mail) {
-        return cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(mail))
-                .orElseGet(() -> null);
+        Optional<EclipseUser> u =cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(mail));
+        if (u.isPresent()) {
+            LOGGER.debug("Found user with email {}", mail);
+            return u.get();
+        }
+        LOGGER.debug("Could not find user with email {}", mail);
+        return null;
     }
 
     @Override
     public EclipseUser getUserByGithubUsername(String username) {
-        return cache.get("gh:" + username, new MultivaluedMapImpl<>(), EclipseUser.class,
-                () -> accounts.getUserByGithubUname(getBearerToken(), username)).orElseGet(() -> null);
+        Optional<EclipseUser> u = cache.get("gh:" + username, new MultivaluedMapImpl<>(), EclipseUser.class,
+                () -> accounts.getUserByGithubUname(getBearerToken(), username));
+        if (u.isPresent()) {
+            LOGGER.debug("Found user with name {}", username);
+            return u.get();
+        }
+        LOGGER.debug("Could not find user with name {}", username);
+        return null;
     }
 
     /**
@@ -83,10 +94,8 @@ public class CachedUserService implements UserService {
         // standard user check (returns best match)
         LOGGER.debug("Checking user with mail {}", mail);
         try {
-            List<EclipseUser> matches = cache
-                    .get(mail, null, EclipseUser.class, () -> accounts.getUsers(getBearerToken(), mail))
-                    .orElseGet(Collections::emptyList);
-            if (!matches.isEmpty()) {
+            List<EclipseUser> matches = accounts.getUsers(getBearerToken(), mail);
+            if (matches != null && !matches.isEmpty()) {
                 return matches.get(0);
             }
         } catch (WebApplicationException e) {
-- 
GitLab