diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..f041762a --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +STAGE=dev +GCP_PROJECT_ID=ard-eventhub +FIREBASE_API_KEY= +GOOGLE_APPLICATION_CREDENTIALS=./keys/ingest.json +#PORT= +#DEBUG=true + +TEST_USER= +TEST_USER_PW= +#TEST_USER_RESET=true \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d7e79ba6..19373597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.0-beta2] - 2021-03-25 + +### Added + +- Add trailing 0 in service publisher-ids if number is only 5 digits + ## [1.0.0-beta1] - 2021-03-24 🚧 BREAKING CHANGES for `serviceIds` 🚧 diff --git a/README.md b/README.md index 19b21f33..e1a3544f 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,10 @@ ARD system to distribute real-time (live) metadata for primarily radio broadcast - [Get involved](#get-involved) - [Modules](#modules) - [Ingest](#ingest) + - [Changelog](#changelog) - [License](#license) - [Third-Party Components](#third-party-components) + - [Authors](#authors) ## Get Started and Documentation @@ -33,6 +35,10 @@ This project will include two modules: Ingest and API. The first development ste The Ingest service is responsible for receiving and publishing events and managing subscriptions. You'll find the core code in [`./src/ingest/`](./src/ingest/). +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for latest changes. + ## License This project is licensed under **European Union Public License 1.2** ([`EUPL-1.2`](https://spdx.org/licenses/EUPL-1.2.html)), which can be found in [LICENSE.txt](LICENSE.txt). Detailed information and translations to all 23 official languages of the European Union are available at [joinup.ec.europa.eu](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12). The usage of this license does not imply any relationship to or endorsement by the European Union, the joinup initiative or other participating parties. @@ -66,7 +72,7 @@ This source code is provided under EUPL v1.2, except for the [`spdx-exceptions`] | NPM DEV | `chai` | [MIT](https://github.com/chaijs/chai/blob/master/LICENSE) | | NPM DEV | `chai-http` | [MIT](https://github.com/chaijs/chai-http/blob/master/package.json) | | NPM DEV | `docsify-cli` | [MIT](https://github.com/docsifyjs/docsify-cli/blob/master/LICENSE) | -| NPM DEV | `dotenv` | [BSD-2-Clause](<[BSD-2-Clause](https://github.com/motdotla/dotenv/blob/master/LICENSE)>) | +| NPM DEV | `dotenv` | [BSD-2-Clause](https://github.com/motdotla/dotenv/blob/master/LICENSE) | | NPM DEV | `eslint` | [MIT](https://github.com/eslint/eslint/blob/master/LICENSE) | | NPM DEV | `eslint-plugin-swr` | [ISC](https://github.com/swrlab/eslint-plugin-swr/blob/main/package.json) | | NPM DEV | `eslint-plugin-chai-friendly` | [MIT](https://github.com/ihordiachenko/eslint-plugin-chai-friendly/blob/master/LICENSE) | @@ -74,3 +80,11 @@ This source code is provided under EUPL v1.2, except for the [`spdx-exceptions`] | NPM DEV | `mocha` | [MIT](https://github.com/mochajs/mocha/blob/master/LICENSE) | | NPM DEV | `nodemon` | [MIT](https://github.com/remy/nodemon/blob/master/LICENSE) | | NPM DEV | `prettier` | [MIT](https://github.com/prettier/prettier/blob/main/LICENSE) | + +## Authors + +This project was realized by + +- [Daniel Freytag](https://github.com/frytg) +- [Rafael Mäuer](https://github.com/rafaelmaeuer) +- [Christian Hufnagel](https://github.com/chhufnagel) diff --git a/openapi.json b/openapi.json index 4881757a..584b850a 100644 --- a/openapi.json +++ b/openapi.json @@ -1,5 +1,5 @@ { - "openapi": "3.0.1", + "openapi": "3.0.3", "info": { "title": "ARD-Eventhub", "description": "ARD system to distribute real-time (live) metadata for primarily radio broadcasts.", @@ -239,6 +239,36 @@ } } } + }, + "401": { + "description": "Missing authentication", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorUnauthorized" + } + } + } + }, + "403": { + "description": "Invalid authorization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorForbidden" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorInternalServerError" + } + } + } } } } @@ -272,6 +302,36 @@ } } } + }, + "401": { + "description": "Missing authentication", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorUnauthorized" + } + } + } + }, + "403": { + "description": "Invalid authorization", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorForbidden" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorInternalServerError" + } + } + } } } } @@ -420,6 +480,16 @@ } } } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorInternalServerError" + } + } + } } } } diff --git a/openapi.yaml b/openapi.yaml index 233fb39c..f355e4d2 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -1,4 +1,4 @@ -openapi: 3.0.1 +openapi: 3.0.3 info: title: ARD-Eventhub description: >- @@ -151,6 +151,24 @@ paths: application/json: schema: $ref: '#/components/schemas/errorBadRequest' + '401': + description: Missing authentication + content: + application/json: + schema: + $ref: '#/components/schemas/errorUnauthorized' + '403': + description: Invalid authorization + content: + application/json: + schema: + $ref: '#/components/schemas/errorForbidden' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/errorInternalServerError' '/events/de.ard.eventhub.v1.radio.track.playing': post: tags: @@ -171,6 +189,24 @@ paths: application/json: schema: $ref: '#/components/schemas/errorBadRequest' + '401': + description: Missing authentication + content: + application/json: + schema: + $ref: '#/components/schemas/errorUnauthorized' + '403': + description: Invalid authorization + content: + application/json: + schema: + $ref: '#/components/schemas/errorForbidden' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/errorInternalServerError' /subscriptions: get: tags: @@ -263,6 +299,12 @@ paths: application/json: schema: $ref: '#/components/schemas/errorNotFound' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/errorInternalServerError' '/subscriptions/{name}': get: tags: diff --git a/package.json b/package.json index 50aff224..73f1dc8f 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "./src/ingest/index.js", "scripts": { "docs:serve": "docsify serve . --port 3000", - "ingest:test": "mocha test/ingest.test.js --timeout 15000 --exit", + "ingest:test": "mocha test/ingest.test.js --timeout 15000 --exit -r dotenv/config", "ingest:local": "nodemon -r dotenv/config ./src/ingest/index.js", - "ingest:cloud": "node ./src/ingest/index.js", + "ingest:cloud": "node -r dotenv/config ./src/ingest/index.js", "license": "chmod +x license.sh && ./license.sh", "test": "mocha test/example.test.js" }, @@ -23,10 +23,10 @@ "@google-cloud/pubsub": "^2.10.0", "body-parser": "^1.19.0", "compression": "^1.7.4", - "dd-trace": "^0.31.2", + "dd-trace": "^0.31.4", "express": "4.17.1", - "express-openapi-validator": "^4.12.6", - "firebase-admin": "^9.5.0", + "express-openapi-validator": "^4.12.7", + "firebase-admin": "^9.6.0", "jsonwebtoken": "^8.5.1", "moment": "^2.29.1", "node-crc": "^1.3.0", @@ -43,10 +43,10 @@ "chai-http": "^4.3.0", "docsify-cli": "^4.4.3", "dotenv": "^8.2.0", - "eslint": "^7.22.0", + "eslint": "^7.23.0", "eslint-plugin-chai-friendly": "^0.6.0", "eslint-plugin-swr": "0.0.5", - "license-compliance": "^1.1.0", + "license-compliance": "^1.2.0", "mocha": "^8.3.2", "nodemon": "^2.0.7", "prettier": "^2.2.1" diff --git a/samples/README.md b/samples/README.md deleted file mode 100644 index 73f346b9..00000000 --- a/samples/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# ARD-Eventhub - -Soon you will find samples on how to publish or receive message in different languages in this folder. diff --git a/src/ingest/README.md b/src/ingest/README.md index e96e56e0..0a0f2cee 100644 --- a/src/ingest/README.md +++ b/src/ingest/README.md @@ -8,13 +8,14 @@ The Ingest service is used to accept incoming events, distribute them via Pub/Su Designated host is Kubernetes but the Docker container will also be used in other environments such as Google Cloud Run for testing purposes. -It needs several environment variables to work: +Several environment variables need to be set in `.env` config in order to run the project: -- REQUIRED `FIREBASE_API_KEY` - corresponding `API_KET` which matches the `GCP_PROJECT_ID` -- REQUIRED `GOOGLE_APPLICATION_CREDENTIALS` - where the Google Cloud Service Account Key can be found (usually a path to a .json file) - REQUIRED `GCP_PROJECT_ID` - which GCP project ID to use for Pub/Sub and Datastore requests -- OPTIONAL `PORT` - override server port setting, default is 8080 +- REQUIRED `FIREBASE_API_KEY` - corresponding `API_KEY` which matches the `GCP_PROJECT_ID` +- REQUIRED `GOOGLE_APPLICATION_CREDENTIALS` - where the Google Cloud Service Account Key can be found (usually a path to a .json file) - REQUIRED `STAGE` - can be one of the Stages below to switch several settings +- OPTIONAL `PORT` - override server port setting, default is 8080 +- OPTIONAL `DEBUG` - set true to enable more detailed logging ### Stages @@ -42,12 +43,9 @@ To run this project locally in your development environment you'll need these pr 6. `roles/monitoring.metricWriter` 7. `roles/pubsub.admin` 3. Install dependencies (`yarn`) -4. Run the project (replace `gcp-project` and `fb-api-key`) +4. Run the project ```sh - STAGE=dev \ - GCP_PROJECT_ID=gcp-project \ - FIREBASE_API_KEY=fb-api-key \ yarn ingest:local ``` diff --git a/src/ingest/events/post.js b/src/ingest/events/post.js index 1d7d783f..802a2a28 100644 --- a/src/ingest/events/post.js +++ b/src/ingest/events/post.js @@ -28,11 +28,11 @@ module.exports = async (req, res) => { const { user } = req // check eventName - if (message.event && eventName !== message.event) { + if (message?.event !== eventName) { // log access attempt logger.log({ level: 'warning', - message: 'User attempted event with missmatching names', + message: 'User attempted event with mismatching names', source, data: { email: req.user.email, @@ -183,7 +183,7 @@ module.exports = async (req, res) => { // handle errors if (messageId === 'TOPIC_ERROR') { // insert error message and empty id - service.topic.status = 'TOPIC_ERROR_1' + service.topic.status = 'TOPIC_ERROR' service.topic.messageId = null } else if (messageId === 'TOPIC_NOT_FOUND') { // fetch publisher @@ -228,7 +228,7 @@ module.exports = async (req, res) => { }) } else { // Update api result that topic was not created - service.topic.status = 'TOPIC_ERROR_2' + service.topic.status = 'TOPIC_NOT_CREATED' logger.log({ level: 'error', diff --git a/src/ingest/router.js b/src/ingest/router.js index 08569aa2..016ea1ac 100644 --- a/src/ingest/router.js +++ b/src/ingest/router.js @@ -64,7 +64,7 @@ router.delete('/subscriptions/:subscriptionName', authVerify, require('./subscri router.get('/topics/', authVerify, require('./topics/list')) router.get('/topics/:topicName', authVerify, require('./topics/list')) -// send healthcheck ok +// send health-check ok router.get(['/', '/health'], (req, res) => { res.sendStatus(200) }) diff --git a/src/utils/user/resolvePermissions.js b/src/utils/user/resolvePermissions.js deleted file mode 100644 index e69de29b..00000000 diff --git a/test/README.md b/test/README.md index 7c56fbfd..644f3467 100644 --- a/test/README.md +++ b/test/README.md @@ -7,27 +7,16 @@ As test-environment [Mocha](https://mochajs.org/) is used in combination with [C ### Environments -It needs several environment variables to work: +In addition to the [ingest-env](../src/ingest/README.md#Environments), following variables are needed in `.env` config for unit tests to work: -- REQUIRED `STAGE` - can be one of the Stages DEV / PROD -- REQUIRED `GCP_PROJECT_ID` - which GCP project ID to use for Pub/Sub and Datastore requests -- REQUIRED `FIREBASE_API_KEY` - corresponding `API_KEY` which matches the `GCP_PROJECT_ID` -- OPTIONAL `PORT` - override server port setting, default is 8080 - REQUIRED `TEST_USER` - test user email - REQUIRED `TEST_USER_PW` - test user password -- OPTIONAL `TEST_USER_RESET` - test email reset (request limit) +- OPTIONAL `TEST_USER_RESET` - set true for email reset (request limit) ## Setup -To run the tests follow the [ingest-setup](../src/ingest/README.md) first - -Run the project (replace `gcp-project`, `fb-api-key`, `test-user-email` and `test-user-password`) +Follow the [ingest-setup](../src/ingest/README.md) first, then run tests with ```sh - STAGE=dev \ - GCP_PROJECT_ID=gcp-project \ - FIREBASE_API_KEY=fb-api-key \ - TEST_USER=test-user-email \ - TEST_USER_PW=test-user-password \ yarn ingest:test ``` diff --git a/test/ingest.test.js b/test/ingest.test.js index 4b753723..3d5c244b 100644 --- a/test/ingest.test.js +++ b/test/ingest.test.js @@ -97,7 +97,7 @@ describe(`POST ${refreshPath}`, () => { // 🚨 firebase limit is 150 requests per day 🚨 const resetPath = '/auth/reset' -if (process.env.TEST_USER_RESET) { +if (process.env.TEST_USER_RESET === true) { describe(`POST ${resetPath}`, () => { it('request password reset email', (done) => { const resetRequest = { @@ -128,7 +128,6 @@ function testEventKeys(body) { const eventName = 'de.ard.eventhub.v1.radio.track.playing' const eventPath = `/events/${eventName}` -const swrTV = '990030' const event = { event: eventName, type: 'music', diff --git a/yarn.lock b/yarn.lock index 56f8560d..8bda5066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1424,10 +1424,10 @@ combined-stream@^1.0.6, combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" -commander@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" - integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== +commander@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== component-emitter@^1.2.0: version "1.3.0" @@ -1600,10 +1600,10 @@ date-and-time@^0.14.2: resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.14.2.tgz#a4266c3dead460f6c231fe9674e585908dac354e" integrity sha512-EFTCh9zRSEpGPmJaexg7HTuzZHh6cnJj1ui7IGCFNXzd2QdpsNh05Db5TF3xzJm30YN+A8/6xHSuRcQqoc3kFA== -dd-trace@^0.31.2: - version "0.31.2" - resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-0.31.2.tgz#0fe6a8720e57b884c8fb2765e9119cc62c895b9d" - integrity sha512-z7nZWJw8q074yM6l8sn+iB4MfF00P1ZheXcnuEc4r/Za4IsUdIXUTIOoXKC6w6wDHt8uQTrVWwAGfUyQJIIneg== +dd-trace@^0.31.4: + version "0.31.4" + resolved "https://registry.yarnpkg.com/dd-trace/-/dd-trace-0.31.4.tgz#0b06ac74fd5cdac5a9c23167c6fbaa0feb0a982f" + integrity sha512-qh/M6TjesnAJDoS9PU3BxOGEHDaFxCpHwaGrf2LqwuB71t3dC2dCZgaLPjVkkjBbObbQ18ZnhWL9b53U4WKn6A== dependencies: "@types/node" "^10.12.18" axios "^0.21.1" @@ -2174,10 +2174,10 @@ eslint-visitor-keys@^2.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== -eslint@^7.22.0: - version "7.22.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f" - integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg== +eslint@^7.23.0: + version "7.23.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.23.0.tgz#8d029d252f6e8cf45894b4bee08f5493f8e94325" + integrity sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q== dependencies: "@babel/code-frame" "7.12.11" "@eslint/eslintrc" "^0.4.0" @@ -2313,10 +2313,10 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== -express-openapi-validator@^4.12.6: - version "4.12.6" - resolved "https://registry.yarnpkg.com/express-openapi-validator/-/express-openapi-validator-4.12.6.tgz#0738a0f7b8fb643839dd8c4a5f95809cb2d51f08" - integrity sha512-F8m1Kp2zNhwGq2a/cu5PJgZkIQs5WyH68nSxH0uIOoHmDF8PPixp2xEtuQVtj/XQeAtBT7Em+7q7GT7ASGjXzg== +express-openapi-validator@^4.12.7: + version "4.12.7" + resolved "https://registry.yarnpkg.com/express-openapi-validator/-/express-openapi-validator-4.12.7.tgz#af7635d5c1890aa749d12bbfa786700e99eb33fa" + integrity sha512-W9IWH7P3L/2NYubhpfXbT2lt1i+U7ZMmAt/UDLn5xDfMYxl7zlXo7LtjcO0hOItYGicFLgQleBsw9cNHDZBkug== dependencies: "@types/multer" "^1.4.5" ajv "^6.12.6" @@ -2481,10 +2481,10 @@ find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -firebase-admin@^9.5.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-9.5.0.tgz#438bc343f1fa0644c2bdbeb36eefec3eda3bf966" - integrity sha512-OPXFOTDcAE+NORpfhq7YMEDk+vFClBtjfpkrjm2JHRxb8DpMm+K3AcusonFPU/WOH4FhiVN9JHB0+NPE20S3gQ== +firebase-admin@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-9.6.0.tgz#eabb0513f5aa0ebc473934669895d2a4d7e4e8a3" + integrity sha512-GNrxsQsZ6alz9u+uYmX84qcixxYQnfOrByxVgEHWiCI9JSCbMOQ/1Px2A6+Coz5zzFokTgXsHnIg+Qz7hMlNZg== dependencies: "@firebase/database" "^0.8.1" "@firebase/database-types" "^0.6.1" @@ -3358,18 +3358,20 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -license-compliance@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/license-compliance/-/license-compliance-1.1.0.tgz#9f906999320afbdb484e5268f4d976a506c306ae" - integrity sha512-krgjD8nt/F80jeyoBzx+wcYmuhrKkI9mi9OiBLSI8cI8UYB1lwYhK7rcTJEJFkgxuyRIcw/yDwb5DYnF8RfGEw== +license-compliance@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/license-compliance/-/license-compliance-1.2.0.tgz#d45a68ff49ed6d9a7cfb567bb2105f99b5560bd5" + integrity sha512-rEyvY4L8Wom6zt6R8HN1C59EEMBc9EBTpaWdtrcNNeWpxFCOc6fA/ETCRzuX3IKJ/J3e4AnUijKGZ9t2W9eZbw== dependencies: chalk "4.1.0" - commander "7.1.0" + commander "7.2.0" cosmiconfig "7.0.0" debug "4.3.1" joi "17.4.0" spdx-expression-parse "3.0.1" spdx-satisfies "5.0.0" + tslib "2.1.0" + xmlbuilder "15.1.1" limiter@^1.1.4: version "1.1.5" @@ -5086,6 +5088,11 @@ tsconfig-paths@^3.9.0: minimist "^1.2.0" strip-bom "^3.0.0" +tslib@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" + integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + tslib@^1.11.1, tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" @@ -5369,6 +5376,11 @@ xdg-basedir@^4.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== +xmlbuilder@15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"