diff --git a/.env.tests b/.env.tests index 67af142f..0fe7df5c 100644 --- a/.env.tests +++ b/.env.tests @@ -24,4 +24,7 @@ SYNAPSE_OAUTH_ISSUER=SAMPLE_ISSUER USER_OFFICE_CORE_EXCHANGE_NAME=user_office_backend.fanout PROPOSAL_CREATION_QUEUE_NAME=connector.proposal_creation.queue CHATROOM_CREATION_QUEUE_NAME=consumer.chatroom_creation.queue -FOLDER_CREATION_QUEUE_NAME=connector.proposals_folders_creation.queue \ No newline at end of file +FOLDER_CREATION_QUEUE_NAME=connector.proposals_folders_creation.queue +VISA_QUEUE_NAME=dummy +VISA_SYNCING_TRIGGERING_STATUSES="ALLOCATED" +VISA_DATABASE_URL=postgres://postgres:qwerty123@127.0.0.1:5434/visa \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index e559f277..f196f653 100644 --- a/.eslintrc +++ b/.eslintrc @@ -52,7 +52,10 @@ ], "quotes": [ "error", - "single" + "single", + { + "avoidEscape": true + } ], "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/naming-convention": [ diff --git a/.github/workflows/auto-merge-dependabot.yml b/.github/workflows/auto-merge-dependabot.yml new file mode 100644 index 00000000..cc5eeb8f --- /dev/null +++ b/.github/workflows/auto-merge-dependabot.yml @@ -0,0 +1,35 @@ +name: Dependabot auto approve and merge + +on: pull_request + +permissions: + pull-requests: write + contents: write + packages: read + +jobs: + auto-approve-and-merge: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + steps: + # Extract information about the dependencies being updated by a Dependabot-generated PR + - name: Dependabot metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v1.6.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # NOTE: This step is only required if the repository has been configured to Require approval + # Checks if update-type is patch or minor, then approve if the PR status is not approved yet. + - name: Auto approve patch and minor updates + uses: hmarr/auto-approve-action@v4 + if: ${{steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' || steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor'}} + + # NOTE: Requirements for merge has to be configured in the Branch protection rule page. + # To do so, go to repository > Settings > Branches > Edit + - name: Enable auto-merge for Dependabot PRs + if: ${{ contains(github.event.pull_request.title, 'bump')}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index 8258b3cc..34e526f5 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -40,7 +40,9 @@ jobs: with: images: ghcr.io/userofficeproject/connector flavor: latest=true # adds :latest tag to outputs.tags - tags: type=sha,format=long,prefix= # adds : tag to outputs.tags. + tags: | # adds : tag to outputs.tags + type=sha,format=long,prefix= + type=raw,value=${{ steps.extract_branch.outputs.branch }} - name: Build and push id: docker_build @@ -52,6 +54,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} - name: Trigger pipeline + if: ${{ steps.extract_branch.outputs.branch == 'staging' }} # Only trigger on staging branch uses: swapActions/trigger-swap-deployment@v1 with: repository: ${{ github.event.repository.name }} diff --git a/.husky/pre-commit b/.husky/pre-commit index 22e8b022..08404245 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx lint-staged --shell +#!/bin/sh +npx --no-install lint-staged \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index bd3ec80e..e8064497 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18-alpine AS build-stage +FROM node:22-alpine AS build-stage USER node @@ -14,7 +14,7 @@ COPY --chown=node:node . . RUN npm run build -FROM node:18-alpine +FROM node:22-alpine USER node diff --git a/Dockerfile.connector.dev b/Dockerfile.connector.dev new file mode 100644 index 00000000..0abffcf5 --- /dev/null +++ b/Dockerfile.connector.dev @@ -0,0 +1,11 @@ +FROM node:22-alpine + +USER node + +RUN mkdir -p /home/node/app + +WORKDIR /home/node/app + +COPY --chown=node:node package*.json ./ + +RUN npm ci --loglevel error --no-fund diff --git a/README.md b/README.md index cf549ace..3c673db8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The connector connects other services through the message queue. # Requirements -This service requires node =>18.0.0 +This service requires node >=22.0.0 ## Getting started diff --git a/package-lock.json b/package-lock.json index 28856e8f..f39edfdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,39 +10,44 @@ "license": "MIT", "dependencies": { "@user-office-software/duo-logger": "^2.2.1", - "@user-office-software/duo-message-broker": "^1.6.0", - "axios": "^1.6.5", - "dotenv": "^16.3.1", + "@user-office-software/duo-message-broker": "^1.7.0", + "axios": "^1.12.0", + "dotenv": "^16.4.5", "envalid": "^8.0.0", - "express": "^4.18.3", + "express": "^4.21.2", "kafkajs": "^2.2.3", + "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "reflect-metadata": "^0.2.1", + "pg": "^8.14.1", + "prom-client": "^15.1.3", + "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", "tsyringe": "^4.8.0" }, "devDependencies": { "@types/express": "^4.17.21", - "@types/jest": "^29.5.12", - "@types/node": "^20.11.22", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "eslint": "^8.57.0", + "@types/jest": "^29.5.14", + "@types/knex": "^0.16.1", + "@types/node": "^22.13.9", + "@types/pg": "^8.11.11", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unused-imports": "^3.1.0", - "husky": "^9.0.11", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-unused-imports": "^3.2.0", + "husky": "^9.1.6", "jest": "^29.7.0", - "lint-staged": "^15.2.2", - "prettier": "3.2.4", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "lint-staged": "^15.4.3", + "prettier": "3.3.3", + "ts-jest": "^29.3.0", + "typescript": "^5.7.2" }, "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node": ">=22.0.0", + "npm": ">=10.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -178,6 +183,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -675,11 +681,12 @@ } }, "node_modules/@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -785,9 +792,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.1.tgz", - "integrity": "sha512-O7x6dMstWLn2ktjcoiNLDkAGG2EjveHL+Vvc+n0fXumkJYAcSqcVYKtwDU+hDZ0uDUsnUagSYaZrOLAYE8un1A==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -817,21 +824,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -853,9 +861,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -897,10 +906,11 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -1349,10 +1359,18 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgr/core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", - "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", "dev": true, "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" @@ -1361,6 +1379,12 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -1535,27 +1559,31 @@ } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, - "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "dev": true - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/knex": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.16.1.tgz", + "integrity": "sha512-54gWD1HWwdVx5iLHaJ1qxH3I6KyBsj5fFqzRpXFn7REWiEB2jwspeVCombNsocSrqPd7IRPqKrsIME7/cD+TFQ==", + "deprecated": "This is a stub types definition. knex provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "knex": "*" + } + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -1563,11 +1591,24 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", - "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/pg": { + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", + "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" } }, "node_modules/@types/qs": { @@ -1587,12 +1628,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", - "dev": true - }, "node_modules/@types/serve-static": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", @@ -1635,25 +1670,24 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, + "peer": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1669,144 +1703,20 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1822,16 +1732,16 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1839,12 +1749,12 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1852,22 +1762,22 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1880,16 +1790,16 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1897,18 +1807,19 @@ } }, "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -1921,16 +1832,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", - "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/visitor-keys": "5.60.1" + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1938,18 +1849,18 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -1964,53 +1875,36 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "node_modules/@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2022,61 +1916,20 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -2088,83 +1941,39 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/types": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", - "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", - "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/visitor-keys": "5.60.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@typescript-eslint/utils": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", - "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.60.1", - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/typescript-estree": "5.60.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", - "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.60.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "7.16.1", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -2191,9 +2000,9 @@ } }, "node_modules/@user-office-software/duo-message-broker": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@user-office-software/duo-message-broker/-/duo-message-broker-1.6.0.tgz", - "integrity": "sha512-sdU9qi23WoPOP8W+H3eF9qM5fdqQBtT3/nN+y0jFUykrA1vG25ZfEW1bC9sVWwBoofhGpnewi58IUngghdsg4A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@user-office-software/duo-message-broker/-/duo-message-broker-1.7.0.tgz", + "integrity": "sha512-OK8PhXxvNlR/PFugrVr1SYPkGZvgPmDMcSSxeC6cGVsSmLSvXYzz0YKoa8zblxPwXPcWC77yy4gPGb7jIzq1cg==", "dependencies": { "@types/amqplib": "^0.10.1", "@user-office-software/duo-logger": "^2.1.1", @@ -2220,6 +2029,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2354,13 +2164,16 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2372,15 +2185,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -2400,16 +2214,17 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2455,17 +2270,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -2475,16 +2291,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2493,12 +2319,13 @@ } }, "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -2599,9 +2426,10 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -2611,10 +2439,15 @@ "node": ">=8" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -2624,7 +2457,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -2648,20 +2481,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "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==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "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==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2686,6 +2520,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -2747,19 +2582,37 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", @@ -2881,15 +2734,16 @@ "dev": true }, "node_modules/cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2900,6 +2754,7 @@ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" @@ -2912,10 +2767,11 @@ } }, "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2924,16 +2780,18 @@ } }, "node_modules/cli-truncate/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/cli-truncate/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -2951,6 +2809,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3013,12 +2872,14 @@ "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3027,12 +2888,13 @@ } }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/concat-map": { @@ -3085,9 +2947,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -3142,6 +3004,57 @@ "node": ">= 8" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3188,16 +3101,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -3221,6 +3137,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3293,14 +3210,28 @@ } }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/dynamic-dedupe": { @@ -3316,6 +3247,21 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.470", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.470.tgz", @@ -3341,9 +3287,9 @@ "dev": true }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -3359,6 +3305,19 @@ "node": ">=8.12" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3369,50 +3328,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -3421,15 +3387,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3465,7 +3461,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -3488,16 +3483,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -3547,6 +3543,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3575,9 +3572,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -3601,34 +3598,36 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -3662,19 +3661,20 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "27.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", - "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^5.10.0" + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^16.10.0 || ^18.12.0 || >=20.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", - "eslint": "^7.0.0 || ^8.0.0", + "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", "jest": "*" }, "peerDependenciesMeta": { @@ -3687,13 +3687,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3717,9 +3718,9 @@ } }, "node_modules/eslint-plugin-unused-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", - "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz", + "integrity": "sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==", "dev": true, "dependencies": { "eslint-rule-composer": "^0.3.0" @@ -3746,28 +3747,6 @@ "node": ">=4.0.0" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-scope/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -3796,6 +3775,14 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3880,7 +3867,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", @@ -3939,36 +3927,36 @@ } }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -3977,6 +3965,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -4098,10 +4090,41 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4110,12 +4133,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -4175,9 +4198,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -4203,12 +4226,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4311,10 +4337,11 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -4323,14 +4350,24 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4340,11 +4377,23 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "engines": { "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -4358,13 +4407,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -4373,6 +4423,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4455,11 +4510,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4496,20 +4552,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -4518,9 +4575,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4529,12 +4587,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -4544,9 +4601,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -4585,12 +4642,12 @@ } }, "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", "dev": true, "bin": { - "husky": "bin.mjs" + "husky": "bin.js" }, "engines": { "node": ">=18" @@ -4611,9 +4668,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -4678,12 +4735,12 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -4691,6 +4748,14 @@ "node": ">= 0.4" } }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4700,14 +4765,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4771,20 +4838,38 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "dependencies": { "has-tostringtag": "^1.0.0" @@ -4834,9 +4919,9 @@ } }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -4894,12 +4979,15 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4948,12 +5036,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -5060,11 +5148,30 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -5622,10 +5729,11 @@ "dev": true }, "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -5692,6 +5800,77 @@ "node": ">=6" } }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5715,12 +5894,16 @@ } }, "node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/lines-and-columns": { @@ -5730,21 +5913,22 @@ "dev": true }, "node_modules/lint-staged": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", - "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", + "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "3.0.0", - "listr2": "8.0.1", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -5757,10 +5941,11 @@ } }, "node_modules/lint-staged/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -5768,6 +5953,24 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/lint-staged/node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -5836,6 +6039,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lint-staged/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, + "license": "MIT" + }, "node_modules/lint-staged/node_modules/npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -5903,16 +6113,17 @@ } }, "node_modules/listr2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", - "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", - "rfdc": "^1.3.0", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, "engines": { @@ -5920,10 +6131,11 @@ } }, "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5936,6 +6148,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5944,16 +6157,18 @@ } }, "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/listr2/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -5971,6 +6186,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -5986,6 +6202,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6031,14 +6248,15 @@ "dev": true }, "node_modules/log-update": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", - "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^6.2.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^7.0.0", + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" }, @@ -6050,25 +6268,27 @@ } }, "node_modules/log-update/node_modules/ansi-escapes": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", - "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^3.0.0" + "environment": "^1.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6081,6 +6301,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6089,16 +6310,18 @@ } }, "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" }, "node_modules/log-update/node_modules/is-fullwidth-code-point": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -6114,6 +6337,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -6126,10 +6350,11 @@ } }, "node_modules/log-update/node_modules/string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -6147,6 +6372,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6157,23 +6383,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -6236,6 +6451,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", @@ -6281,9 +6505,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -6309,12 +6536,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -6360,6 +6587,19 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6477,14 +6717,15 @@ } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -6494,26 +6735,28 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -6522,6 +6765,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6694,9 +6943,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -6707,71 +6956,222 @@ "node": ">=8" } }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/pg": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, "engines": { - "node": ">=8.6" + "node": ">= 8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", "engines": { - "node": ">=0.10" + "node": ">=4.0.0" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "node_modules/pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", "dev": true, "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", "dev": true, "dependencies": { - "find-up": "^4.0.0" + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" }, "engines": { - "node": ">=8" + "node": ">=10" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, + "node_modules/pg/node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/pg/node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", + "node_modules/pg/node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/pg/node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pg/node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, @@ -6809,6 +7209,60 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "dependencies": { + "obuf": "~1.1.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6819,10 +7273,11 @@ } }, "node_modules/prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -6871,6 +7326,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6927,11 +7394,11 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -7015,25 +7482,38 @@ "node": ">=8.10.0" } }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/reflect-metadata": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -7112,21 +7592,51 @@ } }, "node_modules/restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -7146,10 +7656,11 @@ } }, "node_modules/rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" }, "node_modules/rimraf": { "version": "3.0.2", @@ -7190,13 +7701,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -7219,13 +7730,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -7249,13 +7760,11 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -7263,28 +7772,10 @@ "node": ">=10" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -7317,49 +7808,59 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/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==" }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7392,13 +7893,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7430,6 +7935,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -7446,6 +7952,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7458,6 +7965,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7482,6 +7990,14 @@ "source-map": "^0.6.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7559,14 +8075,15 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7576,28 +8093,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7669,9 +8189,9 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -7684,6 +8204,22 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -7704,6 +8240,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7747,9 +8291,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { "node": ">=16" @@ -7759,28 +8303,32 @@ } }, "node_modules/ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.0.tgz", + "integrity": "sha512-4bfGBX7Gd1Aqz3SyeDS9O276wEU/BInZxskPrbhZLyv+c1wskDCqDFMJQJLWrIr/fKoAH4GE5dKUlrdyvo+39A==", "dev": true, + "license": "MIT", "dependencies": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "type-fest": "^4.37.0", + "yargs-parser": "^21.1.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -7790,6 +8338,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -7801,6 +8352,19 @@ } } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -7952,27 +8516,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/tsyringe": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", @@ -8035,29 +8578,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -8067,16 +8611,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -8086,23 +8631,29 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8127,9 +8678,10 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" }, "node_modules/unhomoglyph": { "version": "1.0.6", @@ -8282,16 +8834,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8359,10 +8911,14 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -8521,6 +9077,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -8888,11 +9445,11 @@ } }, "@babel/runtime": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz", - "integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==", + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", + "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==", "requires": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -8978,9 +9535,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.1.tgz", - "integrity": "sha512-O7x6dMstWLn2ktjcoiNLDkAGG2EjveHL+Vvc+n0fXumkJYAcSqcVYKtwDU+hDZ0uDUsnUagSYaZrOLAYE8un1A==", + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true }, "@eslint/eslintrc": { @@ -9001,18 +9558,18 @@ } }, "@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } @@ -9024,9 +9581,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -9062,9 +9619,9 @@ } }, "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "requires": { "argparse": "^1.0.7", @@ -9415,10 +9972,21 @@ "fastq": "^1.6.0" } }, + "@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==" + }, "@pkgr/core": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.0.tgz", - "integrity": "sha512-Zwq5OCzuwJC2jwqmpEQt7Ds1DTi6BWSwoGkbb1n9pO3hzb35BoJELx7c0T23iDkBGkh2e7tvOtjF3tr3OaQHDQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "dev": true }, "@sinclair/typebox": { @@ -9595,27 +10163,30 @@ } }, "@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", "dev": true, "requires": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, - "@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", - "dev": true - }, "@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/knex": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@types/knex/-/knex-0.16.1.tgz", + "integrity": "sha512-54gWD1HWwdVx5iLHaJ1qxH3I6KyBsj5fFqzRpXFn7REWiEB2jwspeVCombNsocSrqPd7IRPqKrsIME7/cD+TFQ==", + "dev": true, + "requires": { + "knex": "*" + } + }, "@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -9623,11 +10194,22 @@ "dev": true }, "@types/node": { - "version": "20.11.22", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", - "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "requires": { + "undici-types": "~6.20.0" + } + }, + "@types/pg": { + "version": "8.11.11", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz", + "integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==", + "dev": true, "requires": { - "undici-types": "~5.26.4" + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^4.0.1" } }, "@types/qs": { @@ -9647,12 +10229,6 @@ "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, - "@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", - "dev": true - }, "@types/serve-static": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", @@ -9682,182 +10258,104 @@ "@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dev": true, - "requires": { - "@types/yargs-parser": "*" - } - }, - "@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, - "@typescript-eslint/eslint-plugin": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.1.tgz", - "integrity": "sha512-zioDz623d0RHNhvx0eesUmGfIjzrk18nSBC8xewepKXbBvN/7c1qImV7Hg8TI1URTxKax7/zxfxj3Uph8Chcuw==", - "dev": true, - "requires": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/type-utils": "7.1.1", - "@typescript-eslint/utils": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" - } - }, - "@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" - } - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } + "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", + "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "dev": true, + "peer": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/type-utils": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.1.tgz", - "integrity": "sha512-ZWUFyL0z04R1nAEgr9e79YtV5LbafdOtN7yapNbn1ansMyaegl2D4bL7vHoJ4HPSc4CaLwuCVas8CVuneKzplQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" } }, "@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", "dev": true, "requires": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" } }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -9866,97 +10364,62 @@ } }, "@typescript-eslint/scope-manager": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", - "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", + "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/visitor-keys": "5.60.1" + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1" } }, "@typescript-eslint/type-utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.1.tgz", - "integrity": "sha512-5r4RKze6XHEEhlZnJtR3GYeCh1IueUHdbrukV2KSlLXaTjuSfeVF8mZUVPLovidCuZfbVjfhi4c0DNSa/Rdg5g==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", + "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "7.16.1", + "@typescript-eslint/utils": "7.16.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/types": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", + "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", + "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.1.1", - "@typescript-eslint/utils": "7.1.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/visitor-keys": "7.16.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.1.tgz", - "integrity": "sha512-cirZpA8bJMRb4WZ+rO6+mnOJrGFDd38WoXCEI57+CYBqta8Yc8aJym2i7vyqLL1vVYljgw0X27axkUXz32T8TA==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1" - } - }, - "@typescript-eslint/types": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.1.tgz", - "integrity": "sha512-KhewzrlRMrgeKm1U9bh2z5aoL4s7K3tK5DwHDn8MHv0yQfWFz/0ZR6trrIHHa5CsF83j/GgHqzdbzCXJ3crx0Q==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.1.tgz", - "integrity": "sha512-9ZOncVSfr+sMXVxxca2OJOPagRwT0u/UHikM2Rd6L/aB+kL/QAuTnsv6MeXtjzCJYb8PzrXarypSGIPx3Jemxw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/visitor-keys": "7.1.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - } - }, - "@typescript-eslint/utils": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.1.tgz", - "integrity": "sha512-thOXM89xA03xAE0lW7alstvnyoBUbBX38YtY+zAUcpRPcq9EIhXPuJ0YTv948MbzmKh6e1AUszn5cBFK49Umqg==", - "dev": true, - "requires": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.1.1", - "@typescript-eslint/types": "7.1.1", - "@typescript-eslint/typescript-estree": "7.1.1", - "semver": "^7.5.4" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.1.tgz", - "integrity": "sha512-yTdHDQxY7cSoCcAtiBzVzxleJhkGB9NncSIyMYe2+OGON1ZsP9zOPws/Pqgopa65jvknOjlk/w7ulPlZ78PiLQ==", - "dev": true, - "requires": { - "@typescript-eslint/types": "7.1.1", - "eslint-visitor-keys": "^3.4.1" - } - }, "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "requires": { "balanced-match": "^1.0.0" } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -9964,51 +10427,26 @@ } } }, - "@typescript-eslint/types": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", - "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", - "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/visitor-keys": "5.60.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - } - }, "@typescript-eslint/utils": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", - "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", + "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", "dev": true, "requires": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.60.1", - "@typescript-eslint/types": "5.60.1", - "@typescript-eslint/typescript-estree": "5.60.1", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.1", + "@typescript-eslint/types": "7.16.1", + "@typescript-eslint/typescript-estree": "7.16.1" } }, "@typescript-eslint/visitor-keys": { - "version": "5.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", - "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", + "version": "7.16.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", + "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.60.1", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "7.16.1", + "eslint-visitor-keys": "^3.4.3" } }, "@ungap/structured-clone": { @@ -10027,9 +10465,9 @@ } }, "@user-office-software/duo-message-broker": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@user-office-software/duo-message-broker/-/duo-message-broker-1.6.0.tgz", - "integrity": "sha512-sdU9qi23WoPOP8W+H3eF9qM5fdqQBtT3/nN+y0jFUykrA1vG25ZfEW1bC9sVWwBoofhGpnewi58IUngghdsg4A==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@user-office-software/duo-message-broker/-/duo-message-broker-1.7.0.tgz", + "integrity": "sha512-OK8PhXxvNlR/PFugrVr1SYPkGZvgPmDMcSSxeC6cGVsSmLSvXYzz0YKoa8zblxPwXPcWC77yy4gPGb7jIzq1cg==", "requires": { "@types/amqplib": "^0.10.1", "@user-office-software/duo-logger": "^2.1.1", @@ -10048,7 +10486,8 @@ "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==" + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -10143,13 +10582,13 @@ "dev": true }, "array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" } }, "array-flatten": { @@ -10158,15 +10597,16 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, "array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" } }, @@ -10177,16 +10617,17 @@ "dev": true }, "array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" } }, "array.prototype.flat": { @@ -10214,38 +10655,48 @@ } }, "arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "requires": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" } }, + "async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", - "dev": true + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } }, "axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", + "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", "requires": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -10325,19 +10776,24 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==" }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -10347,7 +10803,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -10369,20 +10825,20 @@ } }, "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "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==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -10390,6 +10846,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001503", "electron-to-chromium": "^1.4.431", @@ -10439,13 +10896,24 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, "call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" } }, "callsites": { @@ -10520,12 +10988,12 @@ "dev": true }, "cli-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "requires": { - "restore-cursor": "^4.0.0" + "restore-cursor": "^5.0.0" } }, "cli-truncate": { @@ -10539,21 +11007,21 @@ }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "requires": { "emoji-regex": "^10.3.0", @@ -10625,9 +11093,9 @@ } }, "commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "dev": true }, "concat-map": { @@ -10662,9 +11130,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==" }, "cookie-signature": { "version": "1.0.6", @@ -10707,6 +11175,39 @@ "which": "^2.0.1" } }, + "data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, + "data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -10735,13 +11236,13 @@ "dev": true }, "define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" } }, "define-properties": { @@ -10806,9 +11307,19 @@ } }, "dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } }, "dynamic-dedupe": { "version": "0.3.0", @@ -10823,6 +11334,15 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, + "ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "requires": { + "jake": "^10.8.5" + } + }, "electron-to-chromium": { "version": "1.4.470", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.470.tgz", @@ -10842,9 +11362,9 @@ "dev": true }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "envalid": { "version": "8.0.0", @@ -10854,6 +11374,12 @@ "tslib": "2.6.2" } }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -10864,61 +11390,86 @@ } }, "es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "requires": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "requires": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "requires": { + "es-errors": "^1.3.0" } }, "es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "requires": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, "es-shim-unscopables": { @@ -10944,8 +11495,7 @@ "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "escape-html": { "version": "1.0.3", @@ -10959,16 +11509,17 @@ "dev": true }, "eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -11021,6 +11572,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "requires": {} }, "eslint-import-resolver-node": { @@ -11046,9 +11598,9 @@ } }, "eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "requires": { "debug": "^3.2.7" @@ -11066,27 +11618,29 @@ } }, "eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "dependencies": { @@ -11117,28 +11671,28 @@ } }, "eslint-plugin-jest": { - "version": "27.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", - "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "version": "28.11.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.11.0.tgz", + "integrity": "sha512-QAfipLcNCWLVocVbZW8GimKn5p5iiMcgGbRzz8z/P5q7xw+cNEpYqyzFMtIF/ZgF2HLOyy+dYBut+DoYolvqig==", "dev": true, "requires": { - "@typescript-eslint/utils": "^5.10.0" + "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" } }, "eslint-plugin-unused-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.1.0.tgz", - "integrity": "sha512-9l1YFCzXKkw1qtAru1RWUtG2EVDZY0a0eChKXcL+EZ5jitG7qxdctu4RnvhOJHv4xfmUf7h+JJPINlVpGhZMrw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz", + "integrity": "sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==", "dev": true, "requires": { "eslint-rule-composer": "^0.3.0" @@ -11150,30 +11704,17 @@ "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", "dev": true }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - } - } - }, "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, "espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -11274,36 +11815,36 @@ } }, "express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -11411,21 +11952,50 @@ "flat-cache": "^3.0.4" } }, + "filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "requires": { + "minimatch": "^5.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "requires": { "to-regex-range": "^5.0.1" } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -11475,9 +12045,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -11489,12 +12059,14 @@ } }, "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, @@ -11563,27 +12135,41 @@ "dev": true }, "get-east-asian-width": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true }, "get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "requires": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } }, "get-stream": { "version": "6.0.1", @@ -11592,15 +12178,21 @@ "dev": true }, "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" } }, + "getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -11656,12 +12248,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -11688,36 +12277,36 @@ "dev": true }, "has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "requires": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" } }, "has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "requires": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" } }, "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "requires": { "function-bind": "^1.1.2" } @@ -11747,9 +12336,9 @@ "dev": true }, "husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", "dev": true }, "iconv-lite": { @@ -11761,9 +12350,9 @@ } }, "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true }, "import-fresh": { @@ -11807,30 +12396,34 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "requires": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, + "interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, "is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "requires": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" } }, "is-arrayish": { @@ -11872,12 +12465,21 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, - "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "requires": { + "hasown": "^2.0.2" + } + }, + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, "requires": { - "hasown": "^2.0.0" + "is-typed-array": "^1.1.13" } }, "is-date-object": { @@ -11915,9 +12517,9 @@ } }, "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true }, "is-number": { @@ -11951,12 +12553,12 @@ } }, "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" } }, "is-stream": { @@ -11984,12 +12586,12 @@ } }, "is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "requires": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" } }, "is-weakref": { @@ -12071,11 +12673,24 @@ "istanbul-lib-report": "^3.0.0" } }, + "jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "requires": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + } + }, "jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "peer": true, "requires": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -12504,9 +13119,9 @@ "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==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "requires": { "argparse": "^2.0.1" @@ -12553,6 +13168,44 @@ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true }, + "knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "requires": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "dependencies": { + "colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==" + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + } + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -12570,9 +13223,9 @@ } }, "lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "dev": true }, "lines-and-columns": { @@ -12582,29 +13235,38 @@ "dev": true }, "lint-staged": { - "version": "15.2.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.2.tgz", - "integrity": "sha512-TiTt93OPh1OZOsb5B7k96A/ATl2AjIZo+vnzFZ6oHK5FuTk63ByDtxGQpHm+kFETjEWqgkF95M8FRXKR/LEBcw==", + "version": "15.4.3", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.4.3.tgz", + "integrity": "sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==", "dev": true, "requires": { - "chalk": "5.3.0", - "commander": "11.1.0", - "debug": "4.3.4", - "execa": "8.0.1", - "lilconfig": "3.0.0", - "listr2": "8.0.1", - "micromatch": "4.0.5", - "pidtree": "0.6.0", - "string-argv": "0.3.2", - "yaml": "2.3.4" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" }, "dependencies": { "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "dev": true }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, "execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -12646,6 +13308,12 @@ "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true }, + "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 + }, "npm-run-path": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", @@ -12685,23 +13353,23 @@ } }, "listr2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.1.tgz", - "integrity": "sha512-ovJXBXkKGfq+CwmKTjluEqFi3p4h8xvkxGQQAQan22YCgef4KZ1mKGjzfGh6PL6AW5Csw0QiQPNuQyH+6Xk3hA==", + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", "dev": true, "requires": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", - "log-update": "^6.0.0", - "rfdc": "^1.3.0", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "ansi-styles": { @@ -12711,15 +13379,15 @@ "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "requires": { "emoji-regex": "^10.3.0", @@ -12776,31 +13444,31 @@ "dev": true }, "log-update": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", - "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "requires": { - "ansi-escapes": "^6.2.0", - "cli-cursor": "^4.0.0", - "slice-ansi": "^7.0.0", + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" }, "dependencies": { "ansi-escapes": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", - "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, "requires": { - "type-fest": "^3.0.0" + "environment": "^1.0.0" } }, "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true }, "ansi-styles": { @@ -12810,9 +13478,9 @@ "dev": true }, "emoji-regex": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", "dev": true }, "is-fullwidth-code-point": { @@ -12835,9 +13503,9 @@ } }, "string-width": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "requires": { "emoji-regex": "^10.3.0", @@ -12854,12 +13522,6 @@ "ansi-regex": "^6.0.1" } }, - "type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", - "dev": true - }, "wrap-ansi": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", @@ -12910,6 +13572,11 @@ "tmpl": "1.0.5" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "matrix-events-sdk": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz", @@ -12949,9 +13616,9 @@ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -12971,12 +13638,12 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" } }, @@ -13004,6 +13671,12 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -13088,39 +13761,45 @@ } }, "object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" } }, "object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, "object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, "on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -13242,9 +13921,9 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "path-type": { "version": "4.0.0", @@ -13252,6 +13931,117 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pg": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.14.1.tgz", + "integrity": "sha512-0TdbqfjwIun9Fm/r89oB7RFQ0bLgduAhiIqIXOsyKoiC/L54DbuAAzIEN/9Op0f1Po9X7iCPXGoa/Ah+2aI8Xw==", + "requires": { + "pg-cloudflare": "^1.1.1", + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.8.0", + "pg-protocol": "^1.8.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "dependencies": { + "pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "requires": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + } + }, + "postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" + }, + "postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==" + }, + "postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" + }, + "postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "requires": { + "xtend": "^4.0.0" + } + } + } + }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" + }, + "pg-numeric": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", + "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true + }, + "pg-pool": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz", + "integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==", + "requires": {} + }, + "pg-protocol": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", + "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==" + }, + "pg-types": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", + "dev": true, + "requires": { + "pg-int8": "1.0.1", + "pg-numeric": "1.0.2", + "postgres-array": "~3.0.1", + "postgres-bytea": "~3.0.0", + "postgres-date": "~2.1.0", + "postgres-interval": "^3.0.0", + "postgres-range": "^1.1.1" + } + }, + "pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "requires": { + "split2": "^4.1.0" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -13323,6 +14113,45 @@ } } }, + "possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true + }, + "postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true + }, + "postgres-bytea": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", + "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, + "requires": { + "obuf": "~1.1.2" + } + }, + "postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "dev": true + }, + "postgres-interval": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", + "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true + }, + "postgres-range": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -13330,10 +14159,11 @@ "dev": true }, "prettier": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.4.tgz", - "integrity": "sha512-FWu1oLHKCrtpO1ypU6J0SbK2d9Ckwysq6bHj/uaCP26DxrPpppCLQRGVuqAxSTvhF00AcvDRyYrLNW7ocBhFFQ==", - "dev": true + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "peer": true }, "prettier-linter-helpers": { "version": "1.0.0", @@ -13363,6 +14193,15 @@ } } }, + "prom-client": { + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", + "requires": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + } + }, "prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -13400,11 +14239,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "querystringify": { @@ -13459,25 +14298,34 @@ "picomatch": "^2.2.1" } }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "requires": { + "resolve": "^1.20.0" + } + }, "reflect-metadata": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==" + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" } }, "require-directory": { @@ -13531,13 +14379,30 @@ "dev": true }, "restore-cursor": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "requires": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "dependencies": { + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "requires": { + "mimic-function": "^5.0.0" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + } } }, "retry": { @@ -13552,9 +14417,9 @@ "dev": true }, "rfdc": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", - "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, "rimraf": { @@ -13576,13 +14441,13 @@ } }, "safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "requires": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -13601,13 +14466,13 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "requires": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" } }, @@ -13622,35 +14487,15 @@ "integrity": "sha512-RjZyX3nVwJyCuTo5tGPx+PZWkDMCg7oOLpSlhjDdZfwUoNqG1mM8nyj31IGHyaPWXhjbP7cdK3qZ2bmkJ1GzRw==" }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - } - } + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -13682,6 +14527,11 @@ } } }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -13690,37 +14540,39 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "requires": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" } }, "set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "requires": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" } }, "setprototypeof": { @@ -13744,13 +14596,14 @@ "dev": true }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "signal-exit": { @@ -13809,6 +14662,11 @@ "source-map": "^0.6.0" } }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13870,36 +14728,37 @@ } }, "string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" } }, "string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" } }, "strip-ansi": { @@ -13944,15 +14803,28 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, "synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "requires": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, + "tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==" + }, + "tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "requires": { + "bintrees": "1.0.2" + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -13970,6 +14842,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -14001,26 +14878,36 @@ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" }, "ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "requires": {} }, "ts-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", - "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "version": "29.3.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.0.tgz", + "integrity": "sha512-4bfGBX7Gd1Aqz3SyeDS9O276wEU/BInZxskPrbhZLyv+c1wskDCqDFMJQJLWrIr/fKoAH4GE5dKUlrdyvo+39A==", "dev": true, "requires": { - "bs-logger": "0.x", - "fast-json-stable-stringify": "2.x", + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", "jest-util": "^29.0.0", "json5": "^2.2.3", - "lodash.memoize": "4.x", - "make-error": "1.x", - "semver": "^7.5.3", - "yargs-parser": "^21.0.1" + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.1", + "type-fest": "^4.37.0", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "type-fest": { + "version": "4.38.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.38.0.tgz", + "integrity": "sha512-2dBz5D5ycHIoliLYLi0Q2V7KRaDlH0uWIvmk7TYlAg5slqwiPv1ezJdZm1QEM0xgk29oYWMCbIG7E6gHpvChlg==", + "dev": true + } } }, "ts-node": { @@ -14127,23 +15014,6 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } - } - }, "tsyringe": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.8.0.tgz", @@ -14190,56 +15060,61 @@ } }, "typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" } }, "typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "requires": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" } }, "typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" } }, "typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "requires": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" } }, "typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==" + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==" }, "unbox-primitive": { "version": "1.0.2", @@ -14254,9 +15129,9 @@ } }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "unhomoglyph": { "version": "1.0.6", @@ -14367,16 +15242,16 @@ } }, "which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" } }, "wrap-ansi": { @@ -14423,9 +15298,9 @@ "dev": true }, "yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index d75847dc..f1291f4b 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,8 @@ "main": "src/index.ts", "scripts": { "start": "npm run fix-olm-dependency && tsc && node ./build/src/index.js", - "start:kafka": "tsc && node ./build/src/kafkaProducerTesting/kafkaMockProducer.js", "test": "env NODE_ENV=test jest --config ./jest.unit.config.js --colors", - "debug": "ts-node-dev --inspect --respawn -T ./src/index.ts", + "debug": "ts-node-dev --inspect --respawn --exit-child --watch src --transpile-only src/index.ts", "fix-olm-dependency": "cp -r ./src/@matrix-org ./node_modules/", "build": "npm run fix-olm-dependency && rm -rf ./build && tsc", "lint": "npm run fix-olm-dependency && tsc --noEmit && eslint . --ext .js,.ts --quiet", @@ -19,40 +18,45 @@ "service" ], "engines": { - "npm": ">=9.0.0", - "node": ">=18.0.0" + "npm": ">=10.0.0", + "node": ">=22.0.0" }, - "author": "SWAP", + "author": "SIMS", "license": "MIT", "devDependencies": { "@types/express": "^4.17.21", - "@types/jest": "^29.5.12", - "@types/node": "^20.11.22", - "@typescript-eslint/eslint-plugin": "^7.1.1", - "@typescript-eslint/parser": "^7.1.1", - "eslint": "^8.57.0", + "@types/jest": "^29.5.14", + "@types/knex": "^0.16.1", + "@types/node": "^22.13.9", + "@types/pg": "^8.11.11", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.18.0", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.9.0", - "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unused-imports": "^3.1.0", - "husky": "^9.0.11", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-unused-imports": "^3.2.0", + "husky": "^9.1.6", "jest": "^29.7.0", - "lint-staged": "^15.2.2", - "prettier": "3.2.4", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "lint-staged": "^15.4.3", + "prettier": "3.3.3", + "ts-jest": "^29.3.0", + "typescript": "^5.7.2" }, "dependencies": { "@user-office-software/duo-logger": "^2.2.1", - "@user-office-software/duo-message-broker": "^1.6.0", - "axios": "^1.6.5", - "dotenv": "^16.3.1", + "@user-office-software/duo-message-broker": "^1.7.0", + "axios": "^1.12.0", + "dotenv": "^16.4.5", "envalid": "^8.0.0", - "express": "^4.18.3", + "express": "^4.21.2", "kafkajs": "^2.2.3", + "knex": "^3.1.0", "matrix-js-sdk": "24.1.0", - "reflect-metadata": "^0.2.1", + "pg": "^8.14.1", + "prom-client": "^15.1.3", + "reflect-metadata": "^0.2.2", "ts-node-dev": "^2.0.0", "tsyringe": "^4.8.0" } diff --git a/sample.env b/sample.env index 9883bbc6..ba3de4e0 100644 --- a/sample.env +++ b/sample.env @@ -14,10 +14,11 @@ KAFKA_TOPIC=TestTopic SCICAT_BASE_URL=http://127.0.0.1:3200/api/v3 SCICAT_LOGIN_ENDPOINT=/Users/login -SCICAT_USERNAME=admin -SCICAT_PASSWORD=am2jf70TPNZsSan +SCICAT_USERNAME= +SCICAT_PASSWORD= SCICAT_PROPOSAL_TRIGGERING_STATUSES="SCHEDULING, ALLOCATED" +PROPOSAL_FOLDERS_CREATION_GROUP_PREFIX="" PROPOSAL_FOLDERS_CREATION_TRIGGERING_STATUSES="PROPOSAL_ACCEPTED" PROPOSAL_FOLDERS_CREATION_COMMAND="echo \"Instrument: , Year: , Proposal: \"" MOODLE_FOLDERS_CREATION_COMMAND="echo \"UserId: , CourseShortName: \"" @@ -39,10 +40,23 @@ ENABLE_SCICHAT_ROOM_CREATION=false ENABLE_NICOS_TO_SCICHAT_MESSAGES=false ENABLE_PROPOSAL_FOLDERS_CREATION=false ENABLE_MOODLE_FOLDERS_CREATION=false +ENABLE_ONE_IDENTITY_INTEGRATION=false +ENABLE_SYNC_VISA_PROPOSALS=false USER_OFFICE_CORE_EXCHANGE_NAME=user_office_backend.fanout PROPOSAL_CREATION_QUEUE_NAME=connector.proposal_creation.queue CHATROOM_CREATION_QUEUE_NAME=consumer.chatroom_creation.queue FOLDER_CREATION_QUEUE_NAME=connector.proposals_folder_creation.queue MOODLE_EXCHANGE_NAME=moodle.folder-creation.fanout -MOODLE_FOLDER_CREATION_QUEUE_NAME=connector.moodle_folder_creation.queue \ No newline at end of file +MOODLE_FOLDER_CREATION_QUEUE_NAME=connector.moodle_folder_creation.queue +ONE_IDENTITY_INTEGRATION_QUEUE_NAME=connector.one_identity_integration.queue + +ONE_IDENTITY_PROPOSAL_IDENT_ESET_TYPE="Science proposal" +ONE_IDENTITY_APP_SERVER_URL=https://iam-web01-dev.esss.lu.se/AppServerV9 +ONE_IDENTITY_API_USER= +ONE_IDENTITY_API_PASSWORD= + + +VISA_QUEUE_NAME=dummy +VISA_SYNCING_TRIGGERING_STATUSES="ALLOCATED" +VISA_DATABASE_URL=postgres://postgres:qwerty123@127.0.0.1:5434/visa \ No newline at end of file diff --git a/src/config/Tokens.ts b/src/config/Tokens.ts index abb601fe..cb7b3657 100644 --- a/src/config/Tokens.ts +++ b/src/config/Tokens.ts @@ -3,4 +3,9 @@ export const Tokens = { ProvideMessageBroker: Symbol('MessageBroker'), ConfigureLogger: Symbol('ConfigureLogger'), SynapseService: Symbol('SynapseService'), + VisaInstrumentDataSource: Symbol('VisaInstrumentDataSource'), + VisaProposalDataSource: Symbol('VisaProposalDataSource'), + VisaExperimentDataSource: Symbol('VisaExperimentDataSource'), + VisaExperimentUserDataSource: Symbol('VisaExperimentUserDataSource'), + VisaUserDataSource: Symbol('VisaUserDataSource'), }; diff --git a/src/config/dependencyConfigRun.ts b/src/config/dependencyConfigRun.ts index e57b6811..7c6310c2 100644 --- a/src/config/dependencyConfigRun.ts +++ b/src/config/dependencyConfigRun.ts @@ -1,7 +1,12 @@ import 'reflect-metadata'; import { configureGraylogLogger } from './logger/configureGrayLogLogger'; import { Tokens } from './Tokens'; -import { mapValue, str2Bool } from './utils'; +import { mapClass, mapValue, str2Bool } from './utils'; +import VisaPostgresExperimentDataSource from '../datasources/visa/postgres/ExperimentDataSource'; +import VisaPostgresExperimentUserDataSource from '../datasources/visa/postgres/ExperimentUserDataSource'; +import VisaPostgresInstrumentDataSource from '../datasources/visa/postgres/InstrumentDataSource'; +import VisaPostgresProposalDataSource from '../datasources/visa/postgres/ProposalDataSource'; +import VisaPostgresUserDataSource from '../datasources/visa/postgres/UserDataSource'; import getRabbitMqMessageBroker from '../queue/messageBroker/getRabbitMqMessageBroker'; import { SynapseService } from '../services/synapse/SynapseService'; @@ -16,9 +21,38 @@ const enableScichatRoomCreation = str2Bool( process.env.ENABLE_SCICHAT_ROOM_CREATION as string ); +const enableSyncVisaProposals = str2Bool( + process.env.ENABLE_SYNC_VISA_PROPOSALS as string +); + mapValue( Tokens.SynapseService, enableNicosToScichatMessages || enableScichatRoomCreation ? new SynapseService() : {} ); + +mapClass( + Tokens.VisaInstrumentDataSource, + enableSyncVisaProposals ? VisaPostgresInstrumentDataSource : undefined +); + +mapClass( + Tokens.VisaProposalDataSource, + enableSyncVisaProposals ? VisaPostgresProposalDataSource : undefined +); + +mapClass( + Tokens.VisaExperimentDataSource, + enableSyncVisaProposals ? VisaPostgresExperimentDataSource : undefined +); + +mapClass( + Tokens.VisaExperimentUserDataSource, + enableSyncVisaProposals ? VisaPostgresExperimentUserDataSource : undefined +); + +mapClass( + Tokens.VisaUserDataSource, + enableSyncVisaProposals ? VisaPostgresUserDataSource : undefined +); diff --git a/src/config/dependencyConfigTest.ts b/src/config/dependencyConfigTest.ts index 85e5cef3..19481b1e 100644 --- a/src/config/dependencyConfigTest.ts +++ b/src/config/dependencyConfigTest.ts @@ -6,3 +6,9 @@ import getMockMqMessageBroker from '../queue/messageBroker/getMockMessageBroker' mapValue(Tokens.ConfigureLogger, configureConsoleLogger); mapValue(Tokens.ProvideMessageBroker, getMockMqMessageBroker); + +mapValue(Tokens.VisaInstrumentDataSource, undefined); +mapValue(Tokens.VisaProposalDataSource, undefined); +mapValue(Tokens.VisaExperimentDataSource, undefined); +mapValue(Tokens.VisaExperimentUserDataSource, undefined); +mapValue(Tokens.VisaUserDataSource, undefined); diff --git a/src/config/utils.spec.ts b/src/config/utils.spec.ts index bc5cea5b..15880d38 100644 --- a/src/config/utils.spec.ts +++ b/src/config/utils.spec.ts @@ -1,12 +1,52 @@ import { str2Bool } from './utils'; +jest.mock('axios', () => ({ + __esModule: true, + default: jest.fn(() => + Promise.reject({ + response: { + data: { + error: 'Axios Fetch Error', + }, + }, + }) + ), + isAxiosError: jest.requireActual('axios').isAxiosError, + AxiosHeaders: jest.requireActual('axios').AxiosHeaders, +})); -test('Should able to create boolean config', () => { - expect(str2Bool('true')).toBe(true); - expect(str2Bool('True')).toBe(true); - expect(str2Bool('1')).toBe(true); - expect(str2Bool('false')).toBe(false); - expect(str2Bool('False')).toBe(false); - expect(str2Bool('0')).toBe(false); - expect(str2Bool('othervalue')).toBe(false); - expect(str2Bool('10')).toBe(false); +describe('utils', () => { + beforeEach(() => { + jest.resetModules(); + }); + it('Should able to create boolean config', () => { + expect(str2Bool('true')).toBe(true); + expect(str2Bool('True')).toBe(true); + expect(str2Bool('1')).toBe(true); + expect(str2Bool('false')).toBe(false); + expect(str2Bool('False')).toBe(false); + expect(str2Bool('0')).toBe(false); + expect(str2Bool('othervalue')).toBe(false); + expect(str2Bool('10')).toBe(false); + }); + + it('axiosFetch function should throw correct error message', async () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const axiosFetch = require('./utils').axiosFetch; + const url = 'http://example.com'; + const option = { + method: 'GET', + headers: {}, + body: '', + }; + + expect.assertions(1); + + await expect(axiosFetch(url, option)).rejects.toEqual({ + response: { + data: { + error: 'Axios Fetch Error', + }, + }, + }); + }); }); diff --git a/src/config/utils.ts b/src/config/utils.ts index 08a769cb..622e8803 100644 --- a/src/config/utils.ts +++ b/src/config/utils.ts @@ -1,4 +1,4 @@ -import axios, { AxiosHeaders, AxiosRequestConfig } from 'axios'; +import axios, { AxiosHeaders, AxiosRequestConfig, isAxiosError } from 'axios'; import { container, Lifecycle } from 'tsyringe'; export const mapClass = ( @@ -40,17 +40,24 @@ export const axiosFetch = async ( data: body, headers, } as AxiosRequestConfig; - const axiosResponse = await axios(axiosOptions); - - const { data, status, statusText, headers: axiosHeaders } = axiosResponse; - - const responseHeaders = new AxiosHeaders(axiosHeaders as AxiosHeaders); - - const response = new Response(JSON.stringify(data), { - status, - statusText, - headers: responseHeaders, - }); - - return response; + try { + const axiosResponse = await axios(axiosOptions); + + const { data, status, statusText, headers: axiosHeaders } = axiosResponse; + + const responseHeaders = new AxiosHeaders(axiosHeaders as AxiosHeaders); + + const response = new Response(JSON.stringify(data), { + status, + statusText, + headers: responseHeaders, + }); + + return response; + } catch (error: unknown) { + if (isAxiosError(error)) { + throw new Error(error.response?.data.error || error); + } + throw error; + } }; diff --git a/src/config/validateEnv.ts b/src/config/validateEnv.ts index 67855696..df7e4bf8 100644 --- a/src/config/validateEnv.ts +++ b/src/config/validateEnv.ts @@ -16,6 +16,7 @@ function validateEnv() { GRAYLOG_SERVER: str({ default: 'it-graylog.esss.lu.se' }), GRAYLOG_PORT: port({ default: 12201 }), SCICAT_PROPOSAL_TRIGGERING_STATUSES: str({ default: undefined }), + PROPOSAL_FOLDERS_CREATION_GROUP_PREFIX: str({ default: '' }), PROPOSAL_FOLDERS_CREATION_COMMAND: str({ default: undefined }), PROPOSAL_FOLDERS_CREATION_TRIGGERING_STATUSES: str({ default: undefined }), ENABLE_SCICAT_PROPOSAL_UPSERT: bool({ default: false }), @@ -23,11 +24,21 @@ function validateEnv() { ENABLE_NICOS_TO_SCICHAT_MESSAGES: bool({ default: false }), ENABLE_PROPOSAL_FOLDERS_CREATION: bool({ default: false }), ENABLE_MOODLE_FOLDERS_CREATION: bool({ default: false }), + ENABLE_ONE_IDENTITY_INTEGRATION: bool({ default: false }), + ENABLE_SYNC_VISA_PROPOSALS: bool({ default: false }), PROPOSAL_CREATION_QUEUE_NAME: str({ default: undefined }), CHATROOM_CREATION_QUEUE_NAME: str({ default: undefined }), FOLDER_CREATION_QUEUE_NAME: str({ default: undefined }), MOODLE_EXCHANGE_NAME: str({ default: undefined }), MOODLE_FOLDER_CREATION_QUEUE_NAME: str({ default: undefined }), + ONE_IDENTITY_PROPOSAL_IDENT_ESET_TYPE: str({ default: undefined }), + ONE_IDENTITY_APP_SERVER_URL: str({ default: undefined }), + ONE_IDENTITY_API_USER: str({ default: undefined }), + ONE_IDENTITY_API_PASSWORD: str({ default: undefined }), + USER_OFFICE_CORE_EXCHANGE_NAME: str({ default: undefined }), + VISA_QUEUE_NAME: str({ default: undefined }), + VISA_SYNCING_TRIGGERING_STATUSES: str({ default: undefined }), + VISA_DATABASE_URL: str({ default: undefined }), }); } diff --git a/src/datasources/visa/ExperimentDataSource.ts b/src/datasources/visa/ExperimentDataSource.ts new file mode 100644 index 00000000..ed2d9f6f --- /dev/null +++ b/src/datasources/visa/ExperimentDataSource.ts @@ -0,0 +1,12 @@ +import { Experiment } from '../../models/Visa'; + +export interface ExperimentDataSource { + getByProposalId(proposalId: number): Promise; + create({ + proposalPk, + instrumentId, + }: { + proposalPk: number; + instrumentId: number; + }): Promise; +} diff --git a/src/datasources/visa/ExperimentUserDataSource.ts b/src/datasources/visa/ExperimentUserDataSource.ts new file mode 100644 index 00000000..05d1121c --- /dev/null +++ b/src/datasources/visa/ExperimentUserDataSource.ts @@ -0,0 +1,19 @@ +import { ExperimentUser, User } from '../../models/Visa'; + +export interface ExperimentUserDataSource { + create({ + experimentId, + userId, + }: { + experimentId: string; + userId: string; + }): Promise; + delete({ + experimentId, + userId, + }: { + experimentId: string; + userId: string; + }): Promise; + getAllUsersByExperimentId(experimentId: string): Promise; +} diff --git a/src/datasources/visa/InstitutionDataSource.ts b/src/datasources/visa/InstitutionDataSource.ts new file mode 100644 index 00000000..f7566d3f --- /dev/null +++ b/src/datasources/visa/InstitutionDataSource.ts @@ -0,0 +1,18 @@ +import { Employer } from '../../models/Visa'; +export interface CountryPayload { + countryId?: number; + country?: string; +} +export interface InstitutionPayload { + id: number; + name: string; + country?: number; + verified: boolean; + rorId?: string; +} +export interface InstitutionDataSource { + create( + institution: InstitutionPayload, + country: CountryPayload + ): Promise; +} diff --git a/src/datasources/visa/InstrumentDataSource.ts b/src/datasources/visa/InstrumentDataSource.ts new file mode 100644 index 00000000..298d6d3d --- /dev/null +++ b/src/datasources/visa/InstrumentDataSource.ts @@ -0,0 +1,17 @@ +import { Instrument } from '../../models/Visa'; +export interface InstrumentCreationEventPayload { + id: number; + name: string; +} + +export type InstrumentUpdationEventPayload = InstrumentCreationEventPayload; + +export type InstrumentDeletionEventPayload = InstrumentCreationEventPayload; + +export interface InstrumentDataSource { + get(id: number): Promise; + getByShortCode(code: string): Promise; + create(instrument: InstrumentCreationEventPayload): Promise; + update(instrument: InstrumentUpdationEventPayload): Promise; + delete(id: number): Promise; +} diff --git a/src/datasources/visa/ProposalDataSource.ts b/src/datasources/visa/ProposalDataSource.ts new file mode 100644 index 00000000..9d2ceb0d --- /dev/null +++ b/src/datasources/visa/ProposalDataSource.ts @@ -0,0 +1,33 @@ +import { CountryPayload, InstitutionPayload } from './InstitutionDataSource'; +import { ProposalStatusDefaultShortCodes } from '../../models/ProposalMessage'; +import { Instrument, Proposal } from '../../models/Visa'; +import { ValidProposalMessageData } from '../../queue/consumers/utils/validateProposalMessage'; +export interface ProposerPayload { + firstName?: string; + lastName?: string; + email?: string; + id?: string; + oidcSub?: string; + oauthIssuer?: string; + institution?: InstitutionPayload; + country?: CountryPayload; +} +export interface ProposalSubmissionEventPayload { + proposalPk: number; + shortCode: string; + title: string; + abstract: string; + callId: number; + newStatus: ProposalStatusDefaultShortCodes; + submitted: boolean; + proposer?: ProposerPayload; + members: ProposerPayload[]; + instruments: Instrument[]; +} +export type ProposalUpdationEventPayload = ProposalSubmissionEventPayload; +export interface ProposalDataSource { + get(id: number): Promise; + create(proposal: ValidProposalMessageData): Promise; + update(proposal: ValidProposalMessageData): Promise; + delete(id: number): Promise; +} diff --git a/src/datasources/visa/UserDataSource.ts b/src/datasources/visa/UserDataSource.ts new file mode 100644 index 00000000..bf532fcf --- /dev/null +++ b/src/datasources/visa/UserDataSource.ts @@ -0,0 +1,27 @@ +import { User } from '../../models/Visa'; +import { ProposalUser } from '../../queue/consumers/scicat/scicatProposal/dto'; +export interface UserUpdationEventPayload { + id: number; + user_title: string; + firstname: string; + middlename: string; + lastname: string; + preferredname: string; + oidcSub: string; + oauthRefreshToken: string; + oauthAccessToken: string; + oauthIssuer: string; + nationality: number; + organisation: number; + email: string; + emailVerified: boolean; + placeholder: string; + created: string; + updated: string; +} + +export interface UserDataSource { + create(user: ProposalUser): Promise; + update(user: UserUpdationEventPayload): Promise; + delete(userId: number): Promise; +} diff --git a/src/datasources/visa/mock/InstrumentDataSource.ts b/src/datasources/visa/mock/InstrumentDataSource.ts new file mode 100644 index 00000000..719fa4c9 --- /dev/null +++ b/src/datasources/visa/mock/InstrumentDataSource.ts @@ -0,0 +1,23 @@ +import { Instrument } from '../../../models/Visa'; +import { InstrumentDataSource } from '../InstrumentDataSource'; + +export default class MockInstrumentDataSource implements InstrumentDataSource { + async get(id: number): Promise { + return { id, name: 'test', shortCode: 'test' }; + } + + async getByShortCode(code: string): Promise { + return { id: 1, name: code, shortCode: 'test' }; + } + + async create(instrument: Instrument): Promise { + return instrument; + } + + async update(instrument: Instrument): Promise { + return instrument; + } + async delete(id: number): Promise { + return id; + } +} diff --git a/src/datasources/visa/mock/ProposalDataSource.ts b/src/datasources/visa/mock/ProposalDataSource.ts new file mode 100644 index 00000000..0826c702 --- /dev/null +++ b/src/datasources/visa/mock/ProposalDataSource.ts @@ -0,0 +1,20 @@ +import { Proposal } from '../../../models/Visa'; +import { ValidProposalMessageData } from '../../../queue/consumers/utils/validateProposalMessage'; +import { ProposalDataSource } from '../ProposalDataSource'; + +export default class MockProposalDataSource implements ProposalDataSource { + get(id: number): Promise { + throw new Error('Method not implemented.'); + } + async create(proposal: ValidProposalMessageData): Promise { + throw new Error('Method not implemented.'); + } + + async update(proposal: ValidProposalMessageData): Promise { + throw new Error('Method not implemented.'); + } + + async delete(id: number): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/datasources/visa/postgres/ExperimentDataSource.ts b/src/datasources/visa/postgres/ExperimentDataSource.ts new file mode 100644 index 00000000..e9138622 --- /dev/null +++ b/src/datasources/visa/postgres/ExperimentDataSource.ts @@ -0,0 +1,62 @@ +import database from './database'; +import { Experiment } from '../../../models/Visa'; +import { ExperimentDataSource } from '../ExperimentDataSource'; +import { ExperimentRecord, createExperimentObject } from '../records'; + +export default class PostgresExperimentDataSource + implements ExperimentDataSource +{ + private TABLE_NAME = 'experiment'; + + async getByProposalId(proposalPk: number): Promise { + return await database(this.TABLE_NAME) + .where({ + proposal_id: proposalPk, + }) + .first() + .then((experiment: ExperimentRecord | null) => { + return experiment ? createExperimentObject(experiment) : null; + }); + } + + async create({ + proposalPk, + instrumentId, + }: { + proposalPk: number; + instrumentId: number; + }): Promise { + const experimentExists = await database(this.TABLE_NAME).where({ + proposal_id: proposalPk, + }); + + if (experimentExists.length > 0) { + return await database(this.TABLE_NAME) + .where({ + proposal_id: proposalPk, + }) + .update({ + instrument_id: instrumentId, + }) + .returning(['*']) + .then((experiment: ExperimentRecord[]) => { + return createExperimentObject(experiment[0]); + }); + } + + return await database(this.TABLE_NAME) + .insert({ + id: proposalPk, + proposal_id: proposalPk, + instrument_id: instrumentId, + start_date: '2022-01-01 00:00:00', + end_date: new Date( + new Date().setFullYear(new Date().getFullYear() + 1) + ), + }) + .returning(['*']) + .then(async (experiment: ExperimentRecord[]) => { + return createExperimentObject(experiment[0]); + }); + } +} diff --git a/src/datasources/visa/postgres/ExperimentUserDataSource.ts b/src/datasources/visa/postgres/ExperimentUserDataSource.ts new file mode 100644 index 00000000..33299066 --- /dev/null +++ b/src/datasources/visa/postgres/ExperimentUserDataSource.ts @@ -0,0 +1,70 @@ +import database from './database'; +import { ExperimentUser, User } from '../../../models/Visa'; +import { ExperimentUserDataSource } from '../ExperimentUserDataSource'; +import { + ExperimentUserRecord, + UserRecord, + createExperimentUserObject, + createUserObject, +} from '../records'; + +export default class PostgresExperimentUserDataSource + implements ExperimentUserDataSource +{ + private TABLE_NAME = 'experiment_user'; + + async create({ + experimentId, + userId, + }: { + experimentId: string; + userId: string; + }): Promise { + const experimentUserExists = await database(this.TABLE_NAME).where({ + experiment_id: experimentId, + user_id: userId, + }); + if (experimentUserExists.length > 0) { + return experimentUserExists[0]; + } + + return await database(this.TABLE_NAME) + .insert({ + experiment_id: experimentId, + user_id: userId, + }) + .returning(['*']) + .then((experimentUser: ExperimentUserRecord[]) => { + return createExperimentUserObject(experimentUser[0]); + }); + } + + async delete({ + experimentId, + userId, + }: { + experimentId: string; + userId: string; + }): Promise { + return await database(this.TABLE_NAME) + .where({ + experiment_id: experimentId, + user_id: userId, + }) + .del(); + } + + async getAllUsersByExperimentId(experimentId: string): Promise { + return await database(this.TABLE_NAME) + .join('users', 'users.id', '=', this.TABLE_NAME + '.user_id') + .where({ + experiment_id: experimentId, + }) + .select('users.*') + .then((users: UserRecord[]) => { + return users.map((user) => { + return createUserObject(user); + }); + }); + } +} diff --git a/src/datasources/visa/postgres/InstitutionDataSource.ts b/src/datasources/visa/postgres/InstitutionDataSource.ts new file mode 100644 index 00000000..4bc5b919 --- /dev/null +++ b/src/datasources/visa/postgres/InstitutionDataSource.ts @@ -0,0 +1,38 @@ +import database from './database'; +import { Employer } from '../../../models/Visa'; +import { + CountryPayload, + InstitutionDataSource, + InstitutionPayload, +} from '../InstitutionDataSource'; +import { EmployerRecord, createEmployerObject } from '../records'; + +export default class PostgresInstitutionDataSource + implements InstitutionDataSource +{ + private TABLE_NAME = 'employer'; + + async create( + institution: InstitutionPayload, + country: CountryPayload + ): Promise { + const institutionExists = await database(this.TABLE_NAME).where({ + name: institution.name, + }); + + if (institutionExists.length > 0) { + return institutionExists[0]; + } + + // Insert into institution table + return await database(this.TABLE_NAME) + .insert({ + id: institution.id, + name: institution.name, + }) + .returning(['*']) + .then((institution: EmployerRecord[]) => { + return createEmployerObject(institution[0]); + }); + } +} diff --git a/src/datasources/visa/postgres/InstrumentDataSource.ts b/src/datasources/visa/postgres/InstrumentDataSource.ts new file mode 100644 index 00000000..b9abc9df --- /dev/null +++ b/src/datasources/visa/postgres/InstrumentDataSource.ts @@ -0,0 +1,109 @@ +import database from './database'; +import { Instrument } from '../../../models/Visa'; +import { + InstrumentCreationEventPayload, + InstrumentDataSource, + InstrumentUpdationEventPayload, +} from '../InstrumentDataSource'; +import { InstrumentRecord, createInstrumentObject } from '../records'; + +export default class PostgresInstrumentDataSource + implements InstrumentDataSource +{ + private TABLE_NAME = 'instrument'; + + async get(id: number): Promise { + return await database(this.TABLE_NAME) + .where({ + id: id, + }) + .first() + .then((instrument: InstrumentRecord | null) => { + return instrument ? createInstrumentObject(instrument) : null; + }); + } + + async getByShortCode(code: string): Promise { + return await database(this.TABLE_NAME) + .where({ + name: code, + }) + .first() + .then((instrument: InstrumentRecord | null) => { + return instrument ? createInstrumentObject(instrument) : null; + }); + } + async create( + instrument: InstrumentCreationEventPayload + ): Promise { + // Insert if not exists and return the instrument + const instrumentExists = await database(this.TABLE_NAME) + .where({ + name: instrument.name, + id: instrument.id, + }) + .first(); + + if (instrumentExists) { + return createInstrumentObject(instrumentExists); + } + + return await database(this.TABLE_NAME) + .insert({ + id: instrument.id, + name: instrument.name, + }) + .returning(['*']) + .then((instrument: InstrumentRecord[]) => { + return createInstrumentObject(instrument[0]); + }); + } + + async update( + instrument: InstrumentUpdationEventPayload + ): Promise { + const instrumentExists = await database(this.TABLE_NAME) + .where({ + id: instrument.id, + }) + .first(); + + if (instrumentExists) { + return await database(this.TABLE_NAME) + .where({ + id: instrument.id, + }) + .update({ + name: instrument.name, + }) + .returning(['*']) + .then((instrument: InstrumentRecord[]) => { + return createInstrumentObject(instrument[0]); + }); + } + + return await database(this.TABLE_NAME) + .insert({ + id: instrument.id, + name: instrument.name, + }) + .returning(['*']) + .then((instrument: InstrumentRecord[]) => { + return createInstrumentObject(instrument[0]); + }); + } + + async delete(id: number) { + await database(this.TABLE_NAME) + .where({ + id: id, + }) + .delete() + .returning(['*']) + .then((instrument: InstrumentRecord[]) => { + return createInstrumentObject(instrument[0]); + }); + + return id; + } +} diff --git a/src/datasources/visa/postgres/ProposalDataSource.ts b/src/datasources/visa/postgres/ProposalDataSource.ts new file mode 100644 index 00000000..c7e2bf51 --- /dev/null +++ b/src/datasources/visa/postgres/ProposalDataSource.ts @@ -0,0 +1,71 @@ +import database from './database'; +import { Proposal } from '../../../models/Visa'; +import { ValidProposalMessageData } from '../../../queue/consumers/utils/validateProposalMessage'; +import { ProposalDataSource } from '../ProposalDataSource'; +import { ProposalRecord, createProposalObject } from '../records'; +export default class PostgresProposalDataSource implements ProposalDataSource { + private TABLE_NAME = 'proposal'; + + async get(id: number): Promise { + return await database(this.TABLE_NAME) + .where({ + id, + }) + .first() + .then((proposal: ProposalRecord | null) => { + return proposal ? createProposalObject(proposal) : null; + }); + } + async create(proposal: ValidProposalMessageData): Promise { + return await database(this.TABLE_NAME) + .insert({ + id: proposal.proposalPk, + identifier: proposal.shortCode, + title: proposal.title, + summary: proposal.abstract, + }) + .returning(['*']) + .then(async (proposal: ProposalRecord[]) => { + return createProposalObject(proposal[0]); + }); + } + + async update(proposal: ValidProposalMessageData): Promise { + const proposalExists = await database(this.TABLE_NAME) + .where({ + id: proposal.proposalPk, + }) + .first(); + + // Update only if the Proposal exists and submitted + if (proposalExists && proposal.submitted) { + return await database(this.TABLE_NAME) + .where({ + id: proposal.proposalPk, + }) + .update({ + identifier: proposal.proposalPk, + title: proposal.title, + summary: proposal.abstract, + }) + .returning(['*']) + .then((proposal: ProposalRecord[]) => { + return createProposalObject(proposal[0]); + }); + } else return proposalExists; + } + + async delete(id: number) { + await database(this.TABLE_NAME) + .where({ + id: id, + }) + .delete() + .returning(['*']) + .then((proposal: ProposalRecord[]) => { + return createProposalObject(proposal[0]); + }); + + return id; + } +} diff --git a/src/datasources/visa/postgres/UserDataSource.ts b/src/datasources/visa/postgres/UserDataSource.ts new file mode 100644 index 00000000..e65456f4 --- /dev/null +++ b/src/datasources/visa/postgres/UserDataSource.ts @@ -0,0 +1,135 @@ +import database from './database'; +import { Employer, User } from '../../../models/Visa'; +import { ProposalUser } from '../../../queue/consumers/scicat/scicatProposal/dto'; +import { + EmployerRecord, + UserRecord, + createEmployerObject, + createUserObject, +} from '../records'; +import { UserDataSource, UserUpdationEventPayload } from '../UserDataSource'; + +export default class PostgresUserDataSource implements UserDataSource { + private TABLE_NAME = 'users'; + private EMPLOYER_TABLE_NAME = 'employer'; + private USER_ROLE = 'user_role'; + private ROLE = 'role'; + + async create(user: ProposalUser): Promise { + if (!user.email || !user.institution || !user.oidcSub) return null; + const userExists = await database(this.TABLE_NAME) + .where({ + id: user.oidcSub, + }) + .first() + .then((user: UserRecord) => { + return user ? createUserObject(user) : null; + }); + // Create an Employer, if it does not exist + let employer: Employer; + const employerExists = await database(this.EMPLOYER_TABLE_NAME) + .where({ + id: user.institution.id, + }) + .first() + .then((employer: EmployerRecord) => { + return employer ? createEmployerObject(employer) : null; + }); + if (employerExists) { + employer = employerExists; + } else { + employer = await database(this.EMPLOYER_TABLE_NAME) + .insert({ + id: user.institution.id, + name: user.institution.name, + country_code: user.country?.country?.slice(0, 10) ?? '', + }) + .returning(['*']) + .then((employer: EmployerRecord[]) => { + return createEmployerObject(employer[0]); + }); + } + if (userExists) { + // Update user table if the institution_id is different + if (userExists.affiliationId !== employer.id) { + return await database(this.TABLE_NAME) + .where({ + id: user.oidcSub, + }) + .update({ + affiliation_id: employer.id, + }) + .returning('*') + .then((user: UserRecord[]) => { + return userExists; + }); + } else { + return userExists; + } + } else { + // Insert into users table + return await database(this.TABLE_NAME) + .insert({ + id: user.oidcSub, + email: user.email, + first_name: user.firstName ?? '', + last_name: user.lastName ?? '', + instance_quota: 1, + affiliation_id: employer.id, + }) + .returning(['*']) + .then(async (user: UserRecord[]) => { + const scientificComputingRole = await database(this.ROLE) + .where({ + name: 'SCIENTIFIC_COMPUTING', + }) + .first(); + + if (scientificComputingRole) { + await database(this.USER_ROLE).insert({ + user_id: user[0].id, + role_id: scientificComputingRole.id, + }); + } + + return createUserObject(user[0]); + }); + } + } + + async update(user: UserUpdationEventPayload): Promise { + const userExists = await database(this.TABLE_NAME).where({ + id: user.oidcSub, + }); + + // Update only if the User exists and submitted + if (userExists) { + return await database(this.TABLE_NAME) + .where({ + id: user.oidcSub, + }) + .update({ + first_name: user.firstname ?? '', + last_name: user.lastname ?? '', + }) + .returning(['*']) + .then((user: UserRecord[]) => { + return createUserObject(user[0]); + }); + } else return userExists; + } + + async delete(id: number) { + await database(this.TABLE_NAME) + .where({ + id: id, + }) + .delete() + .returning(['*']) + .then((user: UserRecord[]) => { + return createUserObject(user[0]); + }); + + return id; + } +} diff --git a/src/datasources/visa/postgres/database/index.ts b/src/datasources/visa/postgres/database/index.ts new file mode 100644 index 00000000..09395d04 --- /dev/null +++ b/src/datasources/visa/postgres/database/index.ts @@ -0,0 +1,6 @@ +import LocalDB from './local'; +import RemoteDB from './remote'; + +const database = process.env.NODE_ENV == 'local' ? LocalDB : RemoteDB; + +export default database; diff --git a/src/datasources/visa/postgres/database/local.ts b/src/datasources/visa/postgres/database/local.ts new file mode 100644 index 00000000..03318a11 --- /dev/null +++ b/src/datasources/visa/postgres/database/local.ts @@ -0,0 +1,35 @@ +import { logger } from '@user-office-software/duo-logger'; +import knex from 'knex'; + +const db = knex({ + client: 'postgresql', + connection: process.env.VISA_DATABASE_URL, + pool: { + afterCreate: function (connection: any, done: any) { + const defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + connection.query( + `SET timezone = "${process.env.TZ || defaultTimezone}";`, + function (err: any) { + done(err, connection); + } + ); + }, + }, +}); + +db.on('query-error', function (error: any, obj: any) { + logger.logError('QUERY ERROR', { + message: error?.message, + error, + obj, + QueryName: obj?.sql, + }); +}); + +if (process.env.DATABASE_LOG_QUERIES === '1') { + db.on('query', function ({ sql }: any) { + logger.logDebug(`${new Date().toISOString()} - QUERY`, sql); + }); +} + +export default db; diff --git a/src/datasources/visa/postgres/database/remote.ts b/src/datasources/visa/postgres/database/remote.ts new file mode 100644 index 00000000..51f863d5 --- /dev/null +++ b/src/datasources/visa/postgres/database/remote.ts @@ -0,0 +1,10 @@ +import knex from 'knex'; + +const conString = process.env.VISA_DATABASE_URL; + +const db = knex({ + client: 'pg', + connection: conString, +}); + +export default db; diff --git a/src/datasources/visa/records.ts b/src/datasources/visa/records.ts new file mode 100644 index 00000000..f5054230 --- /dev/null +++ b/src/datasources/visa/records.ts @@ -0,0 +1,123 @@ +import { + Employer, + Experiment, + ExperimentUser, + Instrument, + Proposal, + Role, + User, +} from '../../models/Visa'; + +export interface EmployerRecord { + readonly id: number; + readonly country_code: string; + readonly name: string; + readonly town: string; +} + +export const createEmployerObject = (employer: EmployerRecord) => { + return new Employer( + employer.id, + employer.country_code, + employer.name, + employer.town + ); +}; +export interface InstrumentRecord { + readonly id: number; + readonly name: string; + readonly short_code: string; +} + +export const createInstrumentObject = (instrument: InstrumentRecord) => { + return new Instrument(instrument.id, instrument.name, instrument.short_code); +}; + +export interface ProposalRecord { + readonly id: number; + readonly identifier: string; + readonly public_at: string; + readonly summary: string; + readonly title: string; +} + +export const createProposalObject = (proposal: ProposalRecord) => { + return new Proposal( + proposal.id, + proposal.identifier, + proposal.public_at, + proposal.summary, + proposal.title + ); +}; + +export interface ExperimentRecord { + readonly id: string; + readonly start_date: string; + readonly end_date: string; + readonly proposal_id: number; + readonly instrument_id: number; +} + +export const createExperimentObject = (experiment: ExperimentRecord) => { + return new Experiment( + experiment.id, + experiment.start_date, + experiment.end_date, + experiment.proposal_id, + experiment.instrument_id + ); +}; + +export interface ExperimentUserRecord { + readonly experiment_id: string; + readonly user_id: string; +} + +export const createExperimentUserObject = ( + experimentUser: ExperimentUserRecord +) => { + return new ExperimentUser( + experimentUser.experiment_id, + experimentUser.user_id + ); +}; + +export interface UserRecord { + readonly id: string; + readonly activated_at: string; + readonly email: string; + readonly first_name: string; + readonly last_name: string; + readonly instance_quota: string; + readonly last_seen_at: string; + readonly affiliation_id: number; +} + +export const createUserObject = (user: UserRecord) => { + return new User( + user.id, + user.activated_at, + user.email, + user.first_name, + user.last_name, + user.instance_quota, + user.last_seen_at, + user.affiliation_id + ); +}; + +export interface RoleRecord { + readonly id: number; + readonly description: string; + readonly name: string; +} + +export const createRoleObject = (role: RoleRecord) => { + return new Role(role.id, role.description, role.name); +}; + +export interface RoleUser { + readonly role_id: number; + readonly user_id: number; +} diff --git a/src/index.spec.ts b/src/index.spec.ts index 6d984ed3..99a55385 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -8,6 +8,7 @@ jest.mock('express', () => { }); jest.mock('@user-office-software/duo-logger'); +jest.mock('./middlewares/metrics/metrics', () => jest.fn()); jest.mock('./middlewares/healthCheck', () => jest.fn()); jest.mock('./middlewares/readinessCheck', () => jest.fn()); jest.mock('tsyringe'); diff --git a/src/index.ts b/src/index.ts index 2bea9251..23409f92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { Tokens } from './config/Tokens'; import { str2Bool } from './config/utils'; import validateEnv from './config/validateEnv'; import healthCheck from './middlewares/healthCheck'; +import metrics from './middlewares/metrics/metrics'; import readinessCheck from './middlewares/readinessCheck'; import startKafkaTopicHandling from './queue/kafkaTopicHandling'; import startRabbitMQHandling from './queue/queueHandling'; @@ -26,6 +27,7 @@ async function bootstrap() { nodeVersion: process.version, env: process.env.NODE_ENV, }); + const PORT = process.env.PORT || 4010; const app = express(); @@ -44,6 +46,12 @@ async function bootstrap() { const enableMoodleFoldersCreation = str2Bool( process.env.ENABLE_MOODLE_FOLDERS_CREATION as string ); + const enableOneIdentityIntegration = str2Bool( + process.env.ENABLE_ONE_IDENTITY_INTEGRATION as string + ); + const enableSyncVisaProposals = str2Bool( + process.env.ENABLE_SYNC_VISA_PROPOSALS as string + ); logger.logInfo('Services configuration', { SciCat_Proposal_Upsert: enableScicatProposalUpsert, @@ -51,9 +59,11 @@ async function bootstrap() { Proposal_Folders_Creation: enableProposalFoldersCreation, Nicos_to_Scichat_Messages: enableNicosToScichatMessages, Moodle_Folders_Creation: enableMoodleFoldersCreation, + One_Identity_Integration: enableOneIdentityIntegration, + Sync_Visa_Proposals: enableSyncVisaProposals, }); - app.use(healthCheck()).use(readinessCheck()); + app.use(metrics).use(healthCheck).use(readinessCheck); app.listen(PORT); @@ -67,7 +77,9 @@ async function bootstrap() { enableScicatProposalUpsert || enableScichatRoomCreation || enableProposalFoldersCreation || - enableMoodleFoldersCreation + enableMoodleFoldersCreation || + enableOneIdentityIntegration || + enableSyncVisaProposals ) { startRabbitMQHandling(); } diff --git a/src/kafkaProducerTesting/kafkaMessageProducer.ts b/src/kafkaProducerTesting/kafkaMessageProducer.ts deleted file mode 100644 index 63693c1c..00000000 --- a/src/kafkaProducerTesting/kafkaMessageProducer.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { logger } from '@user-office-software/duo-logger'; -import { Kafka, Producer, ProducerRecord } from 'kafkajs'; - -import { - ProduceType, - SetupConfig, - NicosMessageData, -} from '../models/KafkaTypes'; - -export default class ProducerService { - private kafka: Kafka; - private producer: Producer; - - async setup({ clientId, brokers, retry }: SetupConfig) { - this.kafka = new Kafka({ - clientId, - brokers, - retry, - }); - this.producer = this.kafka.producer(); - } - - async connect() { - try { - logger.logInfo('producer connecting ....', {}); - await this.producer.connect(); - logger.logInfo('producer connected ....', {}); - } catch (reason) { - logger.logError('connect fail', { reason }); - } - } - - async disconnect() { - await this.producer.disconnect(); - } - - async produce(record: ProducerRecord) { - await this.producer.send(record); - } -} - -export class ProduceMessage { - constructor(private readonly _kafka: ProducerService) {} - - async create({ topic, topicMessage }: ProduceType) { - try { - await this._kafka.produce({ - topic: topic, - messages: [ - { - value: Buffer.from( - JSON.stringify({ - proposal: topicMessage.proposal, - instrument: topicMessage.instrument, - source: topicMessage.source, - message: `${ - topicMessage.message - } - ${new Date().toLocaleString()}`, - }) - ), - }, - ], - }); - } catch (error) { - logger.logError('create topic error: ', { error }); - } - } -} - -export const producerConnect = async ({ - topic = '', - messages, - msgSendingInterval = 5000, -}: { - topic?: string; - messages: NicosMessageData; - msgSendingInterval: number; -}) => { - const producer = new ProducerService(); - const produce = new ProduceMessage(producer); - - await producer.setup({ - clientId: process.env.KAFKA_CLIENTID || '', - brokers: [`${process.env.KAFKA_BROKERS}`], - }); - await producer - .connect() - .catch((reason) => - logger.logError('Producer connection error: ', { reason }) - ); - setInterval(() => { - produce - .create({ - topic: process.env.KAFKA_TOPIC || topic, - topicMessage: messages, - }) - .then(() => - logger.logInfo(`Message sent ${new Date().toLocaleString()}`, { - messages, - }) - ) - .catch((reason) => logger.logError('Producer error: ', { reason })); - }, msgSendingInterval); -}; diff --git a/src/kafkaProducerTesting/kafkaMockProducer.ts b/src/kafkaProducerTesting/kafkaMockProducer.ts deleted file mode 100644 index 4e078c92..00000000 --- a/src/kafkaProducerTesting/kafkaMockProducer.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { logger } from '@user-office-software/duo-logger'; -import express from 'express'; - -import '../config'; -import { producerConnect } from './kafkaMessageProducer'; -import validateEnv from '../config/validateEnv'; -import healthCheck from '../middlewares/healthCheck'; -import readinessCheck from '../middlewares/readinessCheck'; - -validateEnv(); - -const topic = process.env.KAFKA_TOPIC; -const messagesForTesting = { - proposal: 'test123456', - instrument: 'scicat instrument', - source: 'NICOS', - message: 'Some messages sent via kafka', -}; - -async function bootstrap() { - const PORT = process.env.PORT || 4011; - const app = express(); - - app.use(healthCheck()).use(readinessCheck()); - - app.listen(PORT); - - process.on('uncaughtException', (error) => { - logger.logException('Unhandled NODE exception', error); - }); - - logger.logInfo(`Running kafka producer service at localhost:${PORT}`, {}); - - await producerConnect({ - topic: topic, - messages: messagesForTesting, - msgSendingInterval: 5000, - }); -} - -bootstrap(); diff --git a/src/middlewares/healthCheck.ts b/src/middlewares/healthCheck.ts index 05b98d5e..401df521 100644 --- a/src/middlewares/healthCheck.ts +++ b/src/middlewares/healthCheck.ts @@ -9,6 +9,4 @@ router.get('/health', (req: Request, res: Response) => { res.status(200).send(JSON.stringify(body)); }); -export default function () { - return router; -} +export default router; diff --git a/src/middlewares/metrics/customMetrics.ts b/src/middlewares/metrics/customMetrics.ts new file mode 100644 index 00000000..e14002b5 --- /dev/null +++ b/src/middlewares/metrics/customMetrics.ts @@ -0,0 +1,18 @@ +import { Counter, Histogram } from 'prom-client'; + +// Metrics for rabbitmq message processing + +// This counter tracks the total number of processed messages, with labels for the queue and status (success or failure) +export const processedMessagesCounter = new Counter({ + name: 'rabbitmq_processed_messages_total', + help: 'Total number of processed messages from RabbitMQ', + labelNames: ['queue', 'status'], +}); + +// This histogram tracks the duration of message processing. It helps monitor how long it takes to process each message. +export const processingDurationHistogram = new Histogram({ + name: 'rabbitmq_message_processing_duration_seconds', + help: 'Duration of message processing in seconds', + labelNames: ['queue'], + buckets: [0.1, 0.5, 1, 5, 10, 30, 60], +}); diff --git a/src/middlewares/metrics/metrics.ts b/src/middlewares/metrics/metrics.ts new file mode 100644 index 00000000..47802e1c --- /dev/null +++ b/src/middlewares/metrics/metrics.ts @@ -0,0 +1,13 @@ +import express, { Request, Response } from 'express'; +import { collectDefaultMetrics, register } from 'prom-client'; + +collectDefaultMetrics(); + +const router = express.Router(); + +router.get('/metrics', async (req: Request, res: Response) => { + res.set('Content-Type', register.contentType); + res.end(await register.metrics()); +}); + +export default router; diff --git a/src/middlewares/readinessCheck.ts b/src/middlewares/readinessCheck.ts index eec32799..5b3a21c1 100644 --- a/src/middlewares/readinessCheck.ts +++ b/src/middlewares/readinessCheck.ts @@ -9,6 +9,4 @@ router.get('/readiness', (req: Request, res: Response) => { res.status(200).send(JSON.stringify(body)); }); -export default function () { - return router; -} +export default router; diff --git a/src/models/Event.ts b/src/models/Event.ts index e7d96120..2ed1cff6 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -1,5 +1,7 @@ export enum Event { - PROPOSAL_STATUS_CHANGED_BY_WORKFLOW = 'PROPOSAL_STATUS_CHANGED_BY_WORKFLOW', - PROPOSAL_STATUS_CHANGED_BY_USER = 'PROPOSAL_STATUS_CHANGED_BY_USER', PROPOSAL_STATUS_ACTION_EXECUTED = 'PROPOSAL_STATUS_ACTION_EXECUTED', + PROPOSAL_ACCEPTED = 'PROPOSAL_ACCEPTED', + PROPOSAL_UPDATED = 'PROPOSAL_UPDATED', + VISIT_CREATED = 'VISIT_CREATED', + VISIT_DELETED = 'VISIT_DELETED', } diff --git a/src/models/ProposalMessage.ts b/src/models/ProposalMessage.ts index b4d42318..b2955ae5 100644 --- a/src/models/ProposalMessage.ts +++ b/src/models/ProposalMessage.ts @@ -3,15 +3,47 @@ import { ProposalUser } from './../queue/consumers/scicat/scicatProposal/dto'; export type Instrument = { id: number; shortCode: string; + allocatedTime: number; }; +export interface InstrumentDto { + _id: string; + id: string; + name: string; + pid: string; + uniqueName: string; + createdBy: string; + updatedBy?: string; + createdAt: string; + updatedAt?: string; + customMetadata: Record; +} + +export enum ProposalStatusDefaultShortCodes { + DRAFT = 'DRAFT', + FEASIBILITY_REVIEW = 'FEASIBILITY_REVIEW', + NOT_FEASIBLE = 'NOT_FEASIBLE', + SEP_SELECTION = 'SEP_SELECTION', + SEP_REVIEW = 'SEP_REVIEW', + ALLOCATED = 'ALLOCATED', + NOT_ALLOCATED = 'NOT_ALLOCATED', + SCHEDULING = 'SCHEDULING', + EXPIRED = 'EXPIRED', + EDITABLE_SUBMITTED = 'EDITABLE_SUBMITTED', + EDITABLE_SUBMITTED_INTERNAL = 'EDITABLE_SUBMITTED_INTERNAL', +} + export type ProposalMessageData = { proposalPk: number; shortCode: string; title: string; abstract: string; - newStatus?: string; + callId: number; + newStatus?: ProposalStatusDefaultShortCodes; + submitted: boolean; members: ProposalUser[]; + dataAccessUsers?: ProposalUser[]; + visitors?: ProposalUser[]; proposer?: ProposalUser; - instrument?: Instrument; + instruments?: Instrument[]; }; diff --git a/src/models/Visa.ts b/src/models/Visa.ts new file mode 100644 index 00000000..beb82af6 --- /dev/null +++ b/src/models/Visa.ts @@ -0,0 +1,71 @@ +export class Experiment { + constructor( + public id: string, + public startDate: string, + public endDate: string, + public instrumentId: number, + public proposalId: number + ) {} +} + +export class Instrument { + constructor( + public id: number, + public name: string, + public shortCode: string + ) {} +} + +export class Proposal { + constructor( + public id: number, + public identifier: string, + public publicAt: string, + public summary: string, + public title: string + ) {} +} + +export class Role { + constructor( + public id: number, + public description: string, + public name: string + ) {} +} + +export class UserRole { + constructor( + public userId: number, + public roleId: number + ) {} +} + +export class User { + constructor( + public id: string, + public activatedAt: string, + public email: string, + public firstName: string, + public lastName: string, + public instanceQuota: string, + public lastSeenAt: string, + public affiliationId: number + ) {} +} + +export class Employer { + constructor( + public id: number, + public countryCode: string, + public name: string, + public town: string + ) {} +} + +export class ExperimentUser { + constructor( + public experimentId: string, + public userId: string + ) {} +} diff --git a/src/queue/consumers/QueueConsumer.spec.ts b/src/queue/consumers/QueueConsumer.spec.ts new file mode 100644 index 00000000..ccfb1c4b --- /dev/null +++ b/src/queue/consumers/QueueConsumer.spec.ts @@ -0,0 +1,105 @@ +jest.mock('@user-office-software/duo-logger'); + +import { logger } from '@user-office-software/duo-logger'; +import { MessageBroker } from '@user-office-software/duo-message-broker'; + +import { QueueConsumer } from './QueueConsumer'; + +class TestQueueConsumer extends QueueConsumer { + getQueueName() { + return 'testQueue'; + } + + getExchangeName() { + return 'testExchange'; + } + + onMessage = jest.fn(); +} + +describe('QueueConsumer', () => { + let messageBrokerMock: jest.Mocked; + let queueConsumer: TestQueueConsumer; + + beforeEach(() => { + messageBrokerMock = { + addQueueToExchangeBinding: jest.fn().mockResolvedValue(undefined), + listenOn: jest.fn().mockResolvedValue(undefined), + } as any; + queueConsumer = new TestQueueConsumer(messageBrokerMock); + }); + + it('should throw error if queue name is not set', async () => { + queueConsumer.getQueueName = jest.fn(); + + await expect(queueConsumer.start()).rejects.toThrow( + `Queue name variable not set for consumer ${queueConsumer.constructor.name}` + ); + }); + + it('should throw error if exchange name is not set', async () => { + queueConsumer.getExchangeName = jest.fn(); + + await expect(queueConsumer.start()).rejects.toThrow( + `Exchange name variable not set for consumer ${queueConsumer.constructor.name}` + ); + }); + + it('should call addQueueToExchangeBinding on start', async () => { + await queueConsumer.start(); + + expect(messageBrokerMock.addQueueToExchangeBinding).toHaveBeenCalledWith( + 'testQueue', + 'testExchange' + ); + }); + + it('should call listenOn on start', async () => { + await queueConsumer.start(); + + expect(messageBrokerMock.listenOn).toHaveBeenCalledWith( + 'testQueue', + expect.any(Function) + ); + }); + + it('should call onMessage when a message is received', async () => { + await queueConsumer.start(); + + await messageBrokerMock.listenOn.mock.calls[0][1]( + 'testMessage', + {}, + {} as any + ); + + expect(logger.logInfo).toHaveBeenCalledWith('Received message on queue', { + queueName: 'testQueue', + }); + expect(logger.logException).not.toHaveBeenCalled(); + expect(queueConsumer.onMessage).toHaveBeenCalledWith('testMessage', {}, {}); + }); + + it('should log error if onMessage throws and rethrow the error', async () => { + const error = new Error('Test error'); + queueConsumer.onMessage = jest.fn().mockRejectedValue(error); + + await queueConsumer.start(); + + await expect( + messageBrokerMock.listenOn.mock.calls[0][1]('testMessage', {}, {} as any) + ).rejects.toThrow(error); + + expect(logger.logInfo).toHaveBeenCalledWith('Received message on queue', { + queueName: 'testQueue', + }); + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling QueueConsumer callback: ', + { + error: error.message, + queue: 'testQueue', + consumer: 'TestQueueConsumer', + args: ['testMessage', {}, {}], + } + ); + }); +}); diff --git a/src/queue/consumers/QueueConsumer.ts b/src/queue/consumers/QueueConsumer.ts index c341ef9a..c98c0796 100644 --- a/src/queue/consumers/QueueConsumer.ts +++ b/src/queue/consumers/QueueConsumer.ts @@ -5,6 +5,11 @@ import { Queue, } from '@user-office-software/duo-message-broker'; +import { + processedMessagesCounter, + processingDurationHistogram, +} from '../../middlewares/metrics/customMetrics'; + export abstract class QueueConsumer { private messageBroker: MessageBroker; @@ -45,8 +50,17 @@ export abstract class QueueConsumer { this.messageBroker.listenOn(queueName as Queue, async (...args) => { logger.logInfo('Received message on queue', { queueName }); + + // Start tracking processing time + const endTimer = processingDurationHistogram.startTimer({ + queue: queueName, + }); + try { await this.onMessage(...args); + + // Increment the success counter + processedMessagesCounter.inc({ queue: queueName, status: 'success' }); } catch (error) { logger.logException('Error while handling QueueConsumer callback: ', { error: (error as Error).message, @@ -54,6 +68,15 @@ export abstract class QueueConsumer { consumer: this.constructor.name, args, }); + + // Increment the failure counter + processedMessagesCounter.inc({ queue: queueName, status: 'failure' }); + + // Re-throw the error to make sure the message is not acknowledged + throw error; + } finally { + // Stop the timer + endTimer(); } }); logger.logInfo('Listening on queue', { queueName }); diff --git a/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.spec.ts b/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.spec.ts index 66a88d3b..c1cd9c2a 100644 --- a/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.spec.ts +++ b/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.spec.ts @@ -1,4 +1,4 @@ -jest.mock('../utils/validateMessages'); +jest.mock('./utils/validateMoodleMessage'); jest.mock('../QueueConsumer', () => ({ QueueConsumer: jest.fn().mockImplementation(() => ({ start: jest.fn(), @@ -10,7 +10,7 @@ import { logger } from '@user-office-software/duo-logger'; import { MessageBroker } from '@user-office-software/duo-message-broker'; import { MoodleFolderCreationQueueConsumer } from './MoodleFolderCreationQueueConsumer'; -import { validateMoodleMessage } from '../utils/validateMessages'; +import { validateMoodleMessage } from './utils/validateMoodleMessage'; describe('MoodleFolderCreationQueueConsumer', () => { let mockLoggerLogError: jest.SpyInstance; diff --git a/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.ts b/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.ts index e04504cd..a9c6abac 100644 --- a/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.ts +++ b/src/queue/consumers/moodle/MoodleFolderCreationQueueConsumer.ts @@ -1,10 +1,10 @@ import { logger } from '@user-office-software/duo-logger'; import { ConsumerCallback } from '@user-office-software/duo-message-broker'; +import { validateMoodleMessage } from './utils/validateMoodleMessage'; import { MoodleMessageData } from '../../../models/MoodleMessage'; import { genericFoldersCreation } from '../generic/genericFoldersCreationCallBack'; import { QueueConsumer } from '../QueueConsumer'; -import { validateMoodleMessage } from '../utils/validateMessages'; const MOODLE_FOLDERS_CREATION_COMMAND = process.env.MOODLE_FOLDERS_CREATION_COMMAND; diff --git a/src/queue/consumers/moodle/utils/validateMoodleMessage.ts b/src/queue/consumers/moodle/utils/validateMoodleMessage.ts new file mode 100644 index 00000000..b21fb9d9 --- /dev/null +++ b/src/queue/consumers/moodle/utils/validateMoodleMessage.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { MoodleMessageData } from '../../../../models/MoodleMessage'; + +// NOTE: +// context can be: instrument_shortCode, course_id +// item can be: (proposal_shortCode, user_id) +export type ValidMoodleMessageData = { + enrolled_user_id: string; + course_short_name: string; +}; + +export function validateMoodleMessage( + moodleMessage: MoodleMessageData +): ValidMoodleMessageData { + if (!moodleMessage.enrolled_user_id) { + throw new Error('Property userid is missing'); + } + + if (!moodleMessage.course_short_name) { + throw new Error('Property courseid is missing'); + } + + return { + enrolled_user_id: moodleMessage.enrolled_user_id, + course_short_name: moodleMessage.course_short_name, + }; +} diff --git a/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts b/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts new file mode 100644 index 00000000..f67ad5f4 --- /dev/null +++ b/src/queue/consumers/nicos/NicosTopicConsumer.spec.ts @@ -0,0 +1,127 @@ +jest.mock('tsyringe', () => ({ + container: { + resolve: jest.fn(), + }, +})); +jest.mock('./utils/validateNicosMessage'); +jest.mock('@user-office-software/duo-logger', () => ({ + logger: { + logError: jest.fn(), + }, +})); + +import { logger } from '@user-office-software/duo-logger'; +import { container } from 'tsyringe'; + +import { TopicSciChatConsumer } from './NicosTopicConsumer'; +import { validateNicosMessage } from './utils/validateNicosMessage'; +import { Tokens } from '../../../config/Tokens'; + +const mockLogin = jest.fn(); +const mockSendMessage = jest.fn(); + +const mockSynapseService = { + login: mockLogin, + sendMessage: mockSendMessage, +}; + +const mockConsume = jest.fn(); + +const mockConsumerService = { + consume: mockConsume, +}; + +describe('TopicSciChatConsumer', () => { + const topic = 'test-topic'; + const kafkaClientId = 'test-client-id'; + const validMessageData = { + proposal: 'p1', + instrument: 'testInstrument', + source: 'test-source', + message: 'hello', + }; + + beforeEach(() => { + jest.clearAllMocks(); + (container.resolve as jest.Mock).mockReturnValue(mockSynapseService); + (validateNicosMessage as jest.Mock).mockReturnValue(validMessageData); + process.env.KAFKA_CLIENTID = kafkaClientId; + }); + + it('should login and consume messages', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + + await consumer.start(topic); + + expect(container.resolve).toHaveBeenCalledWith(Tokens.SynapseService); + expect(mockLogin).toHaveBeenCalledWith('TopicSciChatConsumer'); + expect(mockConsume).toHaveBeenCalledWith( + kafkaClientId, + { topics: [topic] }, + expect.objectContaining({ + eachMessage: expect.any(Function), + }) + ); + }); + + it('should process a valid message and call sendMessage', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + // Simulate eachMessage handler + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from(JSON.stringify(validMessageData)), + offset: '123', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(validateNicosMessage).toHaveBeenCalledWith(validMessageData); + expect(mockSendMessage).toHaveBeenCalledWith('p1', 'hello'); + expect(logger.logError).not.toHaveBeenCalled(); + }); + + it('should log error and not throw if message processing fails', async () => { + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + // Simulate eachMessage handler with invalid JSON + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from('not-json'), + offset: '456', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(logger.logError).toHaveBeenCalledWith('Failed processing message', { + messageOffset: '456', + reason: expect.any(String), + }); + expect(mockSendMessage).not.toHaveBeenCalled(); + }); + + it('should log error if validateNicosMessage throws', async () => { + (validateNicosMessage as jest.Mock).mockImplementation(() => { + throw new Error('validation failed'); + }); + + const consumer = new TopicSciChatConsumer(mockConsumerService as any); + await consumer.start(topic); + + const { eachMessage } = mockConsume.mock.calls[0][2]; + const fakeKafkaMessage = { + value: Buffer.from(JSON.stringify(validMessageData)), + offset: '789', + }; + + await eachMessage({ message: fakeKafkaMessage }); + + expect(logger.logError).toHaveBeenCalledWith('Failed processing message', { + messageOffset: '789', + reason: 'validation failed', + }); + expect(mockSendMessage).not.toHaveBeenCalled(); + }); +}); diff --git a/src/queue/consumers/nicos/NicosTopicConsumer.ts b/src/queue/consumers/nicos/NicosTopicConsumer.ts index 9ba75664..d06bffc5 100644 --- a/src/queue/consumers/nicos/NicosTopicConsumer.ts +++ b/src/queue/consumers/nicos/NicosTopicConsumer.ts @@ -1,12 +1,19 @@ import { logger } from '@user-office-software/duo-logger'; +import { container } from 'tsyringe'; -import { postNicosMessage } from './consumerCallbacks/postNicosMessage'; -import { validateNicosMessage } from './utils/validateNicosMessage'; import ConsumerService from '../KafkaConsumer'; +import { validateNicosMessage } from './utils/validateNicosMessage'; +import { Tokens } from '../../../config/Tokens'; +import { SynapseService } from '../../../services/synapse/SynapseService'; export class TopicSciChatConsumer { constructor(private _consumer: ConsumerService) {} async start(topic: string) { + const synapseService: SynapseService = container.resolve( + Tokens.SynapseService + ); + await synapseService.login('TopicSciChatConsumer'); + this._consumer.consume( `${process.env.KAFKA_CLIENTID}`, { topics: [topic] }, @@ -16,10 +23,10 @@ export class TopicSciChatConsumer { const messageData = JSON.parse(message.value?.toString() as string); const validMessageData = validateNicosMessage(messageData); - await postNicosMessage({ - roomName: validMessageData.proposal, - message: validMessageData.message, - }); + await synapseService.sendMessage( + validMessageData.proposal, + validMessageData.message + ); } catch (error) { logger.logError('Failed processing message', { // Note: offset is similar to the index of the message diff --git a/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts b/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts deleted file mode 100644 index ef9c5a07..00000000 --- a/src/queue/consumers/nicos/consumerCallbacks/postNicosMessage.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { container } from 'tsyringe'; - -import { Tokens } from '../../../../config/Tokens'; -import { SynapseService } from '../../../../services/synapse/SynapseService'; - -const postNicosMessage = async ({ - roomName, - message, -}: { - roomName: string; - message: string; -}) => { - const synapseService: SynapseService = container.resolve( - Tokens.SynapseService - ); - await synapseService.sendMessage(roomName, message); -}; - -export { postNicosMessage }; diff --git a/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts b/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts new file mode 100644 index 00000000..ef273ea1 --- /dev/null +++ b/src/queue/consumers/nicos/utils/validateNicosMessage.spec.ts @@ -0,0 +1,63 @@ +import { validateNicosMessage } from './validateNicosMessage'; +import { NicosMessageData } from '../../../../models/KafkaTypes'; +// Mock NicosMessageData type since it's imported from another file + +describe('validateNicosMessage', () => { + const validMessage: NicosMessageData = { + proposal: 'proposal1', + instrument: 'instrument1', + source: 'source1', + message: 'message1', + }; + + it('should return the message as ValidNicosMessage when all fields are valid', () => { + const result = validateNicosMessage(validMessage); + expect(result).toEqual(validMessage); + }); + + it('should throw error if proposal is missing', () => { + const msg = { ...validMessage, proposal: undefined }; + expect(() => validateNicosMessage(msg as any)).toThrow( + 'Proposal format is wrong' + ); + }); + + it('should throw error if proposal is not a string', () => { + const msg = { ...validMessage, proposal: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('Proposal format is wrong'); + }); + + it('should throw error if instrument is missing', () => { + const msg = { ...validMessage, instrument: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow( + 'Instrument format is wrong' + ); + }); + + it('should throw error if instrument is not a string', () => { + const msg = { ...validMessage, instrument: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow( + 'Instrument format is wrong' + ); + }); + + it('should throw error if source is missing', () => { + const msg = { ...validMessage, source: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow('Source format is wrong'); + }); + + it('should throw error if source is not a string', () => { + const msg = { ...validMessage, source: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('Source format is wrong'); + }); + + it('should throw error if message is missing', () => { + const msg = { ...validMessage, message: undefined as any }; + expect(() => validateNicosMessage(msg)).toThrow('message format is wrong'); + }); + + it('should throw error if message is not a string', () => { + const msg = { ...validMessage, message: 123 as any }; + expect(() => validateNicosMessage(msg)).toThrow('message format is wrong'); + }); +}); diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts new file mode 100644 index 00000000..3bdb9416 --- /dev/null +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.spec.ts @@ -0,0 +1,286 @@ +jest.mock('@user-office-software/duo-logger'); +jest.mock('../QueueConsumer', () => ({ + QueueConsumer: jest.fn().mockImplementation(() => ({ + start: jest.fn(), + })), +})); +jest.mock('./consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'); +jest.mock('./consumerCallbacks/syncVisitToOneIdentityHandler'); +jest.mock('./utils/isVisitMessage'); +jest.mock('axios', () => ({ + isAxiosError: jest.fn(), +})); + +import { logger } from '@user-office-software/duo-logger'; +import { MessageBroker } from '@user-office-software/duo-message-broker'; +import { MessageProperties } from 'amqplib'; +import { isAxiosError } from 'axios'; + +import { syncProposalAndMembersToOneIdentityHandler } from './consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'; +import { syncVisitToOneIdentityHandler } from './consumerCallbacks/syncVisitToOneIdentityHandler'; +import { OneIdentityIntegrationQueueConsumer } from './OneIdentityIntegrationQueueConsumer'; +import { VisitMessage } from './utils/interfaces/VisitMessage'; +import { isVisitMessage } from './utils/isVisitMessage'; +import { Event } from '../../../models/Event'; +import { ProposalMessageData } from '../../../models/ProposalMessage'; + +describe('OneIdentityIntegrationQueueConsumer', () => { + let consumer: OneIdentityIntegrationQueueConsumer; + + beforeEach(() => { + consumer = new OneIdentityIntegrationQueueConsumer({} as MessageBroker); + }); + + describe('onMessage', () => { + it('should not handle message if type is not PROPOSAL_ACCEPTED or PROPOSAL_UPDATED', async () => { + const message = {} as ProposalMessageData; + const type = Event.PROPOSAL_STATUS_ACTION_EXECUTED; + + const result = await consumer.onMessage( + type, + message, + {} as MessageProperties + ); + + expect(result).toBeUndefined(); + expect(logger.logInfo).not.toHaveBeenCalled(); + expect(logger.logException).not.toHaveBeenCalled(); + }); + + it('should not handle message if message is not valid ProposalMessageData', async () => { + const message = {} as ProposalMessageData; + const type = Event.PROPOSAL_ACCEPTED; + + await expect( + consumer.onMessage(type, message, {} as MessageProperties) + ).rejects.toThrow('Invalid proposal message'); + }); + + describe('syncProposalAndMembersToOneIdentityHandler', () => { + it('should call syncProposalAndMembersToOneIdentityHandler and log message handled', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; + + await consumer.onMessage(type, message, {} as MessageProperties); + + expect(syncProposalAndMembersToOneIdentityHandler).toHaveBeenCalledWith( + message, + type + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 1, + 'OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 2, + 'Message handled by OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logException).not.toHaveBeenCalled(); + }); + + it('should log exception and re-throw error if syncProposalAndMembersToOneIdentityHandler throws', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; + const error = new Error('Error'); + + ( + syncProposalAndMembersToOneIdentityHandler as jest.Mock + ).mockRejectedValueOnce(error); + + await expect( + consumer.onMessage(type, message, {} as MessageProperties) + ).rejects.toThrow(error); + + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + error, + { + type, + message, + } + ); + }); + + it('should include Axios error response data in logs when available', async () => { + const message = createProposalMessage({ + shortCode: 'shortCode', + proposerEmail: 'proposer-email', + memberEmails: [], + }); + const type = Event.PROPOSAL_ACCEPTED; + + const axiosError = new Error('Axios Error'); + const mockResponse = { + status: 400, + headers: { 'content-type': 'application/json' }, + data: { message: 'Bad Request' }, + }; + + Object.assign(axiosError, { + isAxiosError: true, + response: mockResponse, + }); + + (isAxiosError as unknown as jest.Mock).mockReturnValueOnce(true); + ( + syncProposalAndMembersToOneIdentityHandler as jest.Mock + ).mockRejectedValueOnce(axiosError); + + await expect( + consumer.onMessage(type, message, {} as MessageProperties) + ).rejects.toThrow(axiosError); + + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + axiosError, + { + type, + message, + response: { + status: mockResponse.status, + headers: mockResponse.headers, + data: mockResponse.data, + }, + } + ); + }); + }); + + describe('syncVisitToOneIdentityHandler', () => { + it('should call syncVisitToOneIdentityHandler and log message handled', async () => { + const message = { + visitorId: 'visitor-id', + startAt: '2021-01-01T00:00:00Z', + endAt: '2021-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, + } as VisitMessage; + const type = Event.VISIT_CREATED; + + (isVisitMessage as unknown as jest.Mock).mockReturnValue(true); + + await consumer.onMessage(type, message as any, {} as MessageProperties); + + expect(syncVisitToOneIdentityHandler).toHaveBeenCalledWith( + message, + type + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 1, + 'OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logInfo).toHaveBeenNthCalledWith( + 2, + 'Message handled by OneIdentityIntegrationQueueConsumer', + { + type, + message, + } + ); + expect(logger.logException).not.toHaveBeenCalled(); + }); + + it('should log exception with Axios error details when syncVisitToOneIdentityHandler throws an Axios error', async () => { + const message = { + visitorId: 'visitor-id', + startAt: '2021-01-01T00:00:00Z', + endAt: '2021-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, + } as VisitMessage; + const type = Event.VISIT_CREATED; + + const axiosError = new Error('Axios Error'); + const mockResponse = { + status: 500, + headers: { 'content-type': 'application/json' }, + data: { message: 'Internal Server Error' }, + }; + + Object.assign(axiosError, { + isAxiosError: true, + response: mockResponse, + }); + + (isVisitMessage as unknown as jest.Mock).mockReturnValue(true); + + (isAxiosError as unknown as jest.Mock).mockReturnValueOnce(true); + (syncVisitToOneIdentityHandler as jest.Mock).mockRejectedValueOnce( + axiosError + ); + + await expect( + consumer.onMessage(type, message as any, {} as MessageProperties) + ).rejects.toThrow(axiosError); + + expect(logger.logException).toHaveBeenCalledWith( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + axiosError, + { + type, + message, + response: { + status: mockResponse.status, + headers: mockResponse.headers, + data: mockResponse.data, + }, + } + ); + }); + + it('should throw an error if the visit message is invalid', async () => { + const message = { some: 'invalid message' }; + const type = Event.VISIT_CREATED; + + (isVisitMessage as unknown as jest.Mock).mockReturnValue(false); + + await expect( + consumer.onMessage(type, message as any, {} as MessageProperties) + ).rejects.toThrow( + `Invalid Visit message received: ${JSON.stringify(message)}` + ); + + expect(syncVisitToOneIdentityHandler).not.toHaveBeenCalled(); + expect(logger.logException).toHaveBeenCalled(); + }); + }); + }); + + function createProposalMessage({ + shortCode, + proposerEmail, + memberEmails, + }: { + shortCode: string; + proposerEmail: string; + memberEmails: string[]; + }): ProposalMessageData { + return { + shortCode, + proposer: { email: proposerEmail, firstName: 'first', lastName: 'last' }, + members: memberEmails.map((email) => ({ email })), + } as ProposalMessageData; + } +}); diff --git a/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts new file mode 100644 index 00000000..0dfaacbb --- /dev/null +++ b/src/queue/consumers/oneidentity/OneIdentityIntegrationQueueConsumer.ts @@ -0,0 +1,107 @@ +import { logger } from '@user-office-software/duo-logger'; +import { ConsumerCallback } from '@user-office-software/duo-message-broker'; +import { isAxiosError } from 'axios'; + +import { QueueConsumer } from '../QueueConsumer'; +import { syncProposalAndMembersToOneIdentityHandler } from './consumerCallbacks/syncProposalAndMembersToOneIdentityHandler'; +import { syncVisitToOneIdentityHandler } from './consumerCallbacks/syncVisitToOneIdentityHandler'; +import { isVisitMessage } from './utils/isVisitMessage'; +import { validateProposalMessage } from './utils/validateProposalMessage'; +import { Event } from '../../../models/Event'; +import { hasTriggeringType } from '../utils/hasTriggeringType'; + +const ONE_IDENTITY_INTEGRATION_QUEUE_NAME = + process.env.ONE_IDENTITY_INTEGRATION_QUEUE_NAME || ''; +const USER_OFFICE_CORE_EXCHANGE_NAME = + process.env.USER_OFFICE_CORE_EXCHANGE_NAME || ''; + +// Events that trigger the handling of syncing proposals and members to One Identity +const SYNC_PROPOSAL_AND_MEMBERS_EVENTS_FOR_HANDLING = [ + Event.PROPOSAL_ACCEPTED, + Event.PROPOSAL_UPDATED, +]; + +// Events that trigger the handling of syncing visits to One Identity +const SYNC_VISIT_EVENTS_FOR_HANDLING = [ + Event.VISIT_CREATED, + Event.VISIT_DELETED, +]; + +// Class for consuming messages from the ESS One Identity Integration Queue +export class OneIdentityIntegrationQueueConsumer extends QueueConsumer { + getQueueName(): string { + return ONE_IDENTITY_INTEGRATION_QUEUE_NAME; + } + + getExchangeName(): string { + return USER_OFFICE_CORE_EXCHANGE_NAME; + } + + onMessage: ConsumerCallback = async (type, message) => { + const eventType = type as Event; + const isProposalEvent = hasTriggeringType( + eventType, + SYNC_PROPOSAL_AND_MEMBERS_EVENTS_FOR_HANDLING + ); + const isVisitEvent = hasTriggeringType( + eventType, + SYNC_VISIT_EVENTS_FOR_HANDLING + ); + + if (!isProposalEvent && !isVisitEvent) return; + + logger.logInfo('OneIdentityIntegrationQueueConsumer', { + type, + message, + }); + + try { + if (isProposalEvent) { + const proposalMessage = validateProposalMessage(message); + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + eventType + ); + } else if (isVisitEvent) { + if (isVisitMessage(message)) + await syncVisitToOneIdentityHandler(message, eventType); + else + throw new Error( + `Invalid Visit message received: ${JSON.stringify(message)}` + ); + } + + logger.logInfo('Message handled by OneIdentityIntegrationQueueConsumer', { + type, + message, + }); + } catch (error) { + const response = extractAxiosErrorResponse(error); + + logger.logException( + 'Error while handling message in OneIdentityIntegrationQueueConsumer', + error, + { + type, + message, + response, + } + ); + + // Re-throw the error to make sure the message is not acknowledged + throw error; + } + }; +} + +function extractAxiosErrorResponse(error: unknown) { + if (isAxiosError(error)) { + return { + status: error.response?.status, + headers: error.response?.headers, + data: error.response?.data, + }; + } + + return undefined; +} diff --git a/src/queue/consumers/oneidentity/README.md b/src/queue/consumers/oneidentity/README.md new file mode 100644 index 00000000..48d8cf10 --- /dev/null +++ b/src/queue/consumers/oneidentity/README.md @@ -0,0 +1,229 @@ +# One Identity Visit Access Management + +## Functional Specification + +### Purpose +The handler manages site and system access in One Identity based on visit creation and deletion events for science users. + +### Process Overview +- When a visit is created, both site access and system access are provisioned in One Identity +- When a visit is deleted, both site access and system access are cancelled in One Identity +- Only science users (users with `CCC_EmployeeSubType === ESSSCIENCEUSER`) are processed + +### Access Types +- **Site Access**: Physical access to facility for the exact visit duration +- **System Access**: Digital access to systems, extends beyond the visit end date by a configurable number of days (default: 30) + +### Key Relationships +- System access is linked to site access via `CustomProperty04` which stores the site access UID +- Both access types are identified by specific roles in `PersonWantsOrgRole` enum +- Proposal's short code is stored in `CustomProperty04` of the system access record + +## Process Flow Chart + +``` +┌─────────────────────────┐ +│ Visit Event Received │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Login to One Identity │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Get Person from ID │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +< Is Science User? >──No──┐ +└──────────┬──────────────┘ │ + Yes │ + ↓ │ +┌─────────────────────────┐ │ +< Event Type? > │ +└──────────┬──────────────┘ │ + │ │ + ┌────────┴────────┐ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│VISIT_CREATED│ │VISIT_DELETED ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│Create Site │ │Find Site ││ +│Access │ │Access ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + ↓ ↓ │ +┌─────────────┐ ┌───────────────┐│ +│Create System│ │Cancel Site ││ +│Access │ │Access ││ +└──────┬──────┘ └───────┬───────┘│ + │ │ │ + │ ↓ │ + │ ┌───────────────┐ │ + │ │Find System │ │ + │ │Access via │ │ + │ │CustomProperty4│ │ + │ └───────┬───────┘ │ + │ │ │ + │ ↓ │ + │ ┌───────────────┐ │ + │ │Cancel System │ │ + │ │Access │ │ + │ └───────┬───────┘ │ + │ │ │ + │ │ │ + └───────┐ │ │ + │ │ │ + │ │ │ + ↓ ↓ │ +┌─────────────────────────┐ │ +│ Logout from One Identity│◄─────┘ +└─────────────────────────┘ +``` + +## Key Implementation Details + +### System Access Duration +- Extends beyond visit by `ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS` (default: 30 days) + +### Access Creation +- Site access matches exact visit dates +- System access starts from the visit start date and extends beyond the visit end date by a configurable number of days (default: 30) +- System access links to site access via `CustomProperty04` + +### Access Cancellation +- System access cancellation depends on finding the parent site access first +- Both must be cancelled + +### Error Handling +- Proper error messages when person or access records are not found +- Always performs logout in finally block to ensure clean session management + +--- + +## One Identity Proposal and Member Sync + +### Purpose +The handler synchronizes proposal information and its members (proposer, co-proposers and data access users) with One Identity. This ensures that proposals and their associated personnel are accurately represented and connected in One Identity. + +### Process Overview +- Triggered by `PROPOSAL_ACCEPTED` and `PROPOSAL_UPDATED` events. +- **Login**: Establishes a session with One Identity. +- **Proposal Retrieval/Creation**: + - For both event types, it first attempts to retrieve the proposal (`ESet`) from One Identity using the proposal data. + - If `PROPOSAL_ACCEPTED` event: + - If the proposal does not exist, it creates the proposal in One Identity. + - If creation fails, an error is thrown. + - If `PROPOSAL_UPDATED` event: + - If the proposal does not exist, the process logs this information and concludes, as there's no existing record to update. +- **User Synchronization**: + - Collects all unique user OIDC sub identifiers from the proposal message (proposer, members and data access users). + - Retrieves the corresponding `UID_Person` for these users from One Identity. + - Logs an error if any users from the proposal message are not found in One Identity. +- **Connection Management**: + - Fetches all existing `PersonHasESET` connections for the identified proposal (`UID_ESet`). + - **Remove Old Connections**: + - Identifies connections in One Identity for persons who are no longer part of the current proposal members/dataAccessUsers list. + - Before removing a connection, it checks if the person has "site access" to the proposal (e.g., as a visitor). + - If the person has site access, their connection to the proposal is *not* removed. + - Otherwise, the outdated connection is removed. + - **Add New Connections**: + - Identifies persons in the current proposal members/dataAccessUsers list who are not yet connected to the proposal in One Identity. + - Creates new `PersonHasESET` connections for these persons. +- **Logout**: Ensures logout from One Identity in a `finally` block, regardless of success or failure. + +### Key Relationships +- **Proposals**: Mapped to `ESet` objects in One Identity. +- **Users**: (Proposers, members) are `Person` objects in One Identity, identified via their OIDC sub. +- **Connections**: The link between a `Person` and an `ESet` is represented by a `PersonHasESET` record. + +### Process Flow Chart + +``` +┌─────────────────────────┐ +│ Proposal Event Received │ +│ (ACCEPTED/UPDATED) │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Login to One Identity │ +└──────────┬──────────────┘ + ↓ +┌─────────────────────────┐ +│ Get Proposal (ESet) │ +│ from One Identity │ +└──────────┬──────────────┘ + │ +┌──────────┴──────────┐ +│ Event Type? │ +└────┬───────────┬────┘ + ↓ ↓ +PROPOSAL_ACCEPTED PROPOSAL_UPDATED + │ │ +┌────┴────────┐ │ ┌──────────────────────────┐ +│ ESet Exists?│ No │ ESet Exists? ├─No─┐ +└──────┬───No─┘ │ └──────────┬───────────────┘ │ + Yes │ Yes │ + │ │ │ ┌──────────────────────────┐ + │ ┌───────┴─────────┐ │ │ Log "Proposal not found │ + │ │ Create ESet in │ │ │ for update", Logout & Exit│ + │ │ One Identity │ │ └──────────────────────────┘ + │ └───────┬─────────┘ │ + │ ↓ │ + │ ┌───────┴─────────┐ │ + │ │ ESet Created? ├─No─┼───►Error: Creation Failed, Logout + │ └───────┬─────────┘ │ + Yes Yes │ + └──────────┼─────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Get UIDs for all proposal users │ +│ (proposer & members) via OIDC sub│ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ All users found in One Identity?├─No─►Log Error: Users Missing +└────────────────┬────────────────┘ + Yes + ↓ +┌─────────────────────────────────┐ +│ Get existing PersonHasESET │ +│ connections for the ESet │ +└────────────────┬────────────────┘ + │ +┌────────────────┴────────────────┐ +│ For each existing connection: │ +│ - Is person still in proposal? │ +│ No ─► Has person site access? │ +│ No ─► Remove Connection │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ For each user in current proposal:│ +│ - Not already connected? │ +│ Yes ─► Add Connection │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Log "Connections updated" │ +└────────────────┬────────────────┘ + ↓ +┌─────────────────────────────────┐ +│ Logout from One Identity │ +└─────────────────────────────────┘ +``` + +### Key Implementation Details +- **User Identification**: Users are primarily identified by their `oidcSub` from the proposal message, which is used to look up their `UID_Person` in One Identity. +- **Proposal Identification**: The proposal itself is identified in One Identity using its `shortCode` and other details from the `ProposalMessageData`. +- **Conditional Connection Removal**: A crucial feature is that connections for users no longer in the proposal are only removed if those users do not also have separate site access to that proposal. This prevents inadvertently revoking access for individuals like visitors who might be associated with a proposal through a different mechanism (site access). +- **Idempotency for `PROPOSAL_ACCEPTED`**: If a `PROPOSAL_ACCEPTED` event is processed for a proposal that already exists in One Identity (e.g., due to a retry), the system does not attempt to re-create it but proceeds to synchronize the member connections. +- **Handling `PROPOSAL_UPDATED` for Non-existent Proposals**: If a `PROPOSAL_UPDATED` event is received for a proposal that isn't found in One Identity, the handler logs this and exits gracefully, as there's no record to update. + +### Error Handling +- **Proposal Creation Failure**: If creating a new proposal (`ESet`) in One Identity fails during a `PROPOSAL_ACCEPTED` event, an error is thrown, and the process is halted. +- **User Not Found**: If any users listed in the proposal message (proposer or members) cannot be found in One Identity, an error is logged. The process continues with the users that were found. +- **Logout Guarantee**: The One Identity session is always closed in a `finally` block, ensuring resources are released even if errors occur during the synchronization process. \ No newline at end of file diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts new file mode 100644 index 00000000..ffc8451b --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.spec.ts @@ -0,0 +1,598 @@ +jest.mock('@user-office-software/duo-logger'); +jest.mock('../utils/ESSOneIdentity', () => ({ + ESSOneIdentity: jest.fn().mockImplementation(() => mockOneIdentity), +})); + +import { logger } from '@user-office-software/duo-logger'; + +import { syncProposalAndMembersToOneIdentityHandler } from './syncProposalAndMembersToOneIdentityHandler'; +import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { ProposalUser } from '../../scicat/scicatProposal/dto'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; +import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; + +const mockOneIdentity: jest.Mocked> = { + login: jest.fn(), + logout: jest.fn(), + getProposal: jest.fn(), + getPerson: jest.fn(), + getPersons: jest.fn(), + createProposal: jest.fn(), + connectPersonToProposal: jest.fn(), + getProposalPersonConnections: jest.fn(), + removeConnectionBetweenPersonAndProposal: jest.fn(), + getPersonWantsOrg: jest.fn(), + createPersonWantsOrg: jest.fn(), + cancelPersonWantsOrg: jest.fn(), + hasPersonSiteAccessToProposal: jest.fn(), +}; + +const setupMocks = (data: { + getProposal: UID_ESet | undefined; + getProposalPersonConnections?: PersonHasESET[]; + getPersons?: string[]; + hasPersonSiteAccessToProposalConfig?: { [key: string]: boolean }; +}) => { + mockOneIdentity.createProposal.mockResolvedValueOnce('proposal-UID_ESet'); + mockOneIdentity.getProposal.mockResolvedValueOnce(data.getProposal); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce( + data.getProposalPersonConnections ?? [] + ); + mockOneIdentity.getPersons.mockResolvedValue( + data.getPersons ?? ['proposer-uid', 'member-uid', 'data-access-uid'] + ); + if (data.hasPersonSiteAccessToProposalConfig) { + mockOneIdentity.hasPersonSiteAccessToProposal.mockImplementation( + async (uidPerson: string, proposalUid: string) => { + void proposalUid; + + return data.hasPersonSiteAccessToProposalConfig?.[uidPerson] ?? false; + } + ); + } else { + mockOneIdentity.hasPersonSiteAccessToProposal.mockResolvedValue(false); + } +}; + +const proposalMessage = { + shortCode: 'shortCode', + proposer: { oidcSub: 'proposer-oidc-sub' }, + members: [{ oidcSub: 'member-oidc-sub' }], + dataAccessUsers: [{ oidcSub: 'data-access-oidc-sub' } as ProposalUser], + visitors: [] as ProposalUser[], +} as ProposalMessageData; + +describe('oneIdentityIntegrationHandler', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + describe('PROPOSAL_ACCEPTED', () => { + it('should handle accepted proposal', async () => { + setupMocks({ + getProposal: undefined, + getProposalPersonConnections: [], + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ); + + expect(mockOneIdentity.createProposal).toHaveBeenCalledWith( + proposalMessage + ); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(0); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(3); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenNthCalledWith( + 1, + 'proposal-UID_ESet', + 'proposer-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenNthCalledWith( + 2, + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenNthCalledWith( + 3, + 'proposal-UID_ESet', + 'data-access-uid' + ); + expect(logger.logError).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should log error if some of the users are not found in One Identity', async () => { + setupMocks({ + getProposal: undefined, + getProposalPersonConnections: [], + getPersons: ['proposer-oidc-sub'], + }); + + const promise = syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ); + + await jest.runAllTimersAsync(); + await promise; + + expect(logger.logError).toHaveBeenCalledWith( + 'discoverOIMPersonsWithRetry: failed after max retries', + expect.objectContaining({ + attempt: 4, + maxRetries: 3, + totalAttempts: 4, + missingCentralAccounts: ['member-oidc-sub', 'data-access-oidc-sub'], + foundCount: 1, + expectedCount: 3, + }) + ); + }); + + it('should retry and eventually find all users after retries', async () => { + setupMocks({ + getProposal: undefined, + getProposalPersonConnections: [], + }); + + // First three attempts return incomplete results, fourth attempt returns all users + mockOneIdentity.getPersons + .mockResolvedValueOnce(['proposer-oidc-sub']) + .mockResolvedValueOnce(['proposer-oidc-sub', 'member-oidc-sub']) + .mockResolvedValueOnce(['proposer-oidc-sub', 'member-oidc-sub']) + .mockResolvedValueOnce([ + 'proposer-oidc-sub', + 'member-oidc-sub', + 'data-access-oidc-sub', + ]); + + const promise = syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ); + + await jest.runAllTimersAsync(); + await promise; + + expect(logger.logWarn).toHaveBeenNthCalledWith( + 1, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 1, + foundCount: 1, + expectedCount: 3, + }) + ); + + expect(logger.logWarn).toHaveBeenNthCalledWith( + 2, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 2, + foundCount: 2, + expectedCount: 3, + }) + ); + + expect(logger.logWarn).toHaveBeenNthCalledWith( + 3, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 3, + delayMs: 60000, + foundCount: 2, + expectedCount: 3, + }) + ); + + // Verify success log on final attempt + expect(logger.logInfo).toHaveBeenCalledWith( + 'discoverOIMPersonsWithRetry: success', + expect.objectContaining({ + attempt: 4, + foundCount: 3, + }) + ); + + // Verify all users are connected + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(3); + }); + + it('should retry three times and fail if users are not found', async () => { + setupMocks({ + getProposal: undefined, + getProposalPersonConnections: [], + getPersons: ['proposer-oidc-sub'], + }); + + const promise = syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ); + + await jest.runAllTimersAsync(); + await promise; + + // Verify that getPersons was called 4 times (initial attempt plus 3 retries) + expect(mockOneIdentity.getPersons).toHaveBeenCalledTimes(4); + + // Verify intermediate retry logs + expect(logger.logWarn).toHaveBeenNthCalledWith( + 1, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 1, + maxRetries: 3, + missingCentralAccounts: ['member-oidc-sub', 'data-access-oidc-sub'], + foundCount: 1, + expectedCount: 3, + }) + ); + + expect(logger.logWarn).toHaveBeenNthCalledWith( + 2, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 2, + maxRetries: 3, + missingCentralAccounts: ['member-oidc-sub', 'data-access-oidc-sub'], + foundCount: 1, + expectedCount: 3, + }) + ); + + expect(logger.logWarn).toHaveBeenNthCalledWith( + 3, + 'discoverOIMPersonsWithRetry: incomplete - retrying', + expect.objectContaining({ + attempt: 3, + maxRetries: 3, + delayMs: 60000, + missingCentralAccounts: ['member-oidc-sub', 'data-access-oidc-sub'], + foundCount: 1, + expectedCount: 3, + }) + ); + + // Verify final error log after max retries exhausted + expect(logger.logError).toHaveBeenCalledWith( + 'discoverOIMPersonsWithRetry: failed after max retries', + expect.objectContaining({ + attempt: 4, + maxRetries: 3, + totalAttempts: 4, + missingCentralAccounts: ['member-oidc-sub', 'data-access-oidc-sub'], + foundCount: 1, + expectedCount: 3, + }) + ); + + // Verify connections are still attempted with partial results + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(1); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'proposer-oidc-sub' + ); + }); + + it('should throw error if proposal creation fails', async () => { + // Set up mocks with getProposal returning undefined (proposal doesn't exist) + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); + + // Mock createProposal to return undefined (creation failed) + mockOneIdentity.createProposal.mockResolvedValueOnce(undefined); + + // Expect the handler to throw an error + await expect( + syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ) + ).rejects.toThrow('Proposal creation failed in ESS One Identity'); + + // Verify that logout is still called (in the finally block) + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + describe('when proposal already exists in One Identity (Retry logic)', () => { + it('should not create proposal but handle connections if proposal exists', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', + }, + ], + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_ACCEPTED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes( + 2 + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'data-access-uid' + ); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); + }); + + describe('PROPOSAL_UPDATED', () => { + it('should handle updated proposal and remove old connections', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'old-member-uid', // this person should be removed + }, + ], + hasPersonSiteAccessToProposalConfig: { 'old-member-uid': false }, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(1); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledWith('proposal-UID_ESet', 'old-member-uid'); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'data-access-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should not remove old connection if person has site access to proposal', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'visitor-member-uid', // this person should NOT be removed due to site access + }, + ], + // 'visitor-member-uid' has site access + hasPersonSiteAccessToProposalConfig: { 'visitor-member-uid': true }, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + // removeConnectionBetweenPersonAndProposal should NOT be called for 'visitor-member-uid' + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalledWith('proposal-UID_ESet', 'visitor-member-uid'); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(0); // No connections should be removed in this specific setup + + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'data-access-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should keep visitor connections when the visitor is still on the proposal', async () => { + const proposalMessageWithVisitor = { + ...proposalMessage, + visitors: [{ oidcSub: 'visitor-oidc-sub' } as ProposalUser], + } as ProposalMessageData; + + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'visitor-uid', + }, + ], + getPersons: [ + 'proposer-uid', + 'member-uid', + 'data-access-uid', + 'visitor-uid', + ], + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessageWithVisitor, + Event.PROPOSAL_UPDATED + ); + + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalledWith('proposal-UID_ESet', 'visitor-uid'); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'data-access-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: [ + 'proposer-uid', + 'member-uid', + 'data-access-uid', + 'visitor-uid', + ], + }); + }); + + it('should remove one old connection and keep another due to site access', async () => { + setupMocks({ + getProposal: 'proposal-UID_ESet', + getProposalPersonConnections: [ + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'proposer-uid', // Keep (in proposal) + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'old-member-to-remove-uid', // Remove (not in proposal, no site access) + }, + { + UID_ESet: 'proposal-UID_ESet', + UID_Person: 'visitor-member-to-keep-uid', // Keep (not in proposal, but has site access) + }, + ], + getPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], // Current members in the proposal message + hasPersonSiteAccessToProposalConfig: { + 'old-member-to-remove-uid': false, + 'visitor-member-to-keep-uid': true, + }, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + 'proposal-UID_ESet' + ); + + // Check removals + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledTimes(1); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledWith('proposal-UID_ESet', 'old-member-to-remove-uid'); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'visitor-member-to-keep-uid' + ); + + // Check additions + // 'member-uid' is in proposalMessage.members and not in initial connections that are kept + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'member-uid' + ); + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + 'proposal-UID_ESet', + 'data-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith('Connections updated', { + uidESet: 'proposal-UID_ESet', + uidPersons: ['proposer-uid', 'member-uid', 'data-access-uid'], + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should not handle proposal if there is no created proposal in One Identity', async () => { + setupMocks({ + getProposal: undefined, + }); + + await syncProposalAndMembersToOneIdentityHandler( + proposalMessage, + Event.PROPOSAL_UPDATED + ); + + expect(mockOneIdentity.createProposal).not.toHaveBeenCalled(); + expect( + mockOneIdentity.getProposalPersonConnections + ).not.toHaveBeenCalled(); + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalled(); + expect(mockOneIdentity.connectPersonToProposal).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith('Proposal in One Identity', { + uidESet: undefined, + }); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts new file mode 100644 index 00000000..034b383d --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncProposalAndMembersToOneIdentityHandler.ts @@ -0,0 +1,211 @@ +import { logger } from '@user-office-software/duo-logger'; + +import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { collectUsersFromProposalMessage } from '../../utils/collectUsersFromProposalMessage'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; +import { UID_Person } from '../utils/interfaces/Person'; +import { PersonHasESET } from '../utils/interfaces/PersonHasESET'; + +export async function syncProposalAndMembersToOneIdentityHandler( + message: ProposalMessageData, + type: Event +): Promise { + // Create a new ESSOneIdentity instance and log in + const oneIdentity = new ESSOneIdentity(); + await oneIdentity.login(); + + logger.logInfo('One Identity successfully logged in', {}); + + try { + const uidESet = await getUIDESetFromOneIdentity(oneIdentity, message, type); + + logger.logInfo('UID_ESet from One Identity', { uidESet }); + + if (uidESet) { + const users = collectUsersFromProposalMessage(message); + await handleConnectionsBetweenProposalAndPersons( + oneIdentity, + uidESet, + users.map((user) => user.oidcSub) + ); + } + } finally { + await oneIdentity.logout(); + logger.logInfo('One Identity successfully logged out', {}); + } +} + +// Method to get UID_ESet from One Identity, create it if it does not exist +async function getUIDESetFromOneIdentity( + oneIdentity: ESSOneIdentity, + message: ProposalMessageData, + type: Event +): Promise { + let uidESet: UID_ESet | undefined = await oneIdentity.getProposal(message); + + logger.logInfo('Proposal in One Identity', { uidESet }); + + if (type === Event.PROPOSAL_UPDATED && !uidESet) { + // Proposal does not exist in One Identity + return; + } + + if (!uidESet) { + // Create proposal in One Identity if it does not exist + uidESet = await oneIdentity.createProposal(message); + + logger.logInfo('UID_ESet created', { uidESet }); + + if (!uidESet) { + throw new Error('Proposal creation failed in ESS One Identity'); + } + } + + return uidESet; +} + +async function discoverPersonsWithRetry( + oneIdentity: ESSOneIdentity, + centralAccounts: string[] +): Promise { + const MAX_RETRIES = 3; + const MAX_ATTEMPTS = MAX_RETRIES + 1; + const RETRY_DELAYS_MS = [20000, 40000, 60000]; // Progressive delays: 20s, 40s, 60s + + let attempts = 0; + + const attemptDiscovery = async (): Promise => { + attempts++; + const uidPersons = await oneIdentity.getPersons(centralAccounts); + + if (uidPersons.length !== centralAccounts.length) { + const missingCentralAccounts = centralAccounts.filter( + (account) => !uidPersons.includes(account) + ); + + if (attempts < MAX_ATTEMPTS) { + const delayMs = RETRY_DELAYS_MS[attempts - 1]; + logger.logWarn('discoverOIMPersonsWithRetry: incomplete - retrying', { + attempt: attempts, + maxRetries: MAX_RETRIES, + delayMs, + missingCentralAccounts, + foundCount: uidPersons.length, + expectedCount: centralAccounts.length, + }); + + await new Promise((resolve) => setTimeout(resolve, delayMs)); + + return attemptDiscovery(); + } else { + logger.logError( + 'discoverOIMPersonsWithRetry: failed after max retries', + { + attempt: attempts, + maxRetries: MAX_RETRIES, + totalAttempts: MAX_ATTEMPTS, + missingCentralAccounts, + foundCount: uidPersons.length, + expectedCount: centralAccounts.length, + } + ); + + return uidPersons; + } + } + + logger.logInfo('discoverOIMPersonsWithRetry: success', { + attempt: attempts, + foundCount: uidPersons.length, + }); + + return uidPersons; + }; + + return attemptDiscovery(); +} + +async function handleConnectionsBetweenProposalAndPersons( + oneIdentity: ESSOneIdentity, + uidESet: UID_ESet, + centralAccounts: string[] +) { + logger.logInfo('Users to be connected to proposal', { + centralAccounts, + }); + + // Get all users from One Identity + const uidPersons = await discoverPersonsWithRetry( + oneIdentity, + centralAccounts + ); + + logger.logInfo('Found persons in One Identity', { uidPersons }); + + // Get all connections between UID_ESet and UID_Person + const connections = await oneIdentity.getProposalPersonConnections(uidESet); + + await removeOldConnections(oneIdentity, connections, uidPersons); + await addNewConnections(oneIdentity, uidESet, connections, uidPersons); + + logger.logInfo('Connections updated', { uidESet, uidPersons }); +} + +async function addNewConnections( + oneIdentity: ESSOneIdentity, + uidESet: UID_ESet, + connections: PersonHasESET[], + uidPersons: UID_Person[] +): Promise { + const connectionsToAdd = uidPersons.filter( + (uidPerson) => + !connections.some((connection) => connection.UID_Person === uidPerson) + ); + + await Promise.all( + connectionsToAdd.map((uidPerson) => + oneIdentity.connectPersonToProposal(uidESet, uidPerson) + ) + ); +} + +async function removeOldConnections( + oneIdentity: ESSOneIdentity, + connections: PersonHasESET[], + uidPersons: UID_Person[] +): Promise { + // Collect connections that are not in the list of current persons (OIM) + const potentiallyRemoveableConnections = connections.filter( + (connection) => !uidPersons.includes(connection.UID_Person) + ); + + const removalChecks = await Promise.all( + potentiallyRemoveableConnections.map(async (connectionToRemove) => { + const hasAccess = await oneIdentity.hasPersonSiteAccessToProposal( + connectionToRemove.UID_Person, + connectionToRemove.UID_ESet + ); + + return { + connection: connectionToRemove, + shouldRemove: !hasAccess, // Remove if the person does NOT have site access + }; + }) + ); + + // Filter out connections that should not be removed + const connectionsToRemove = removalChecks + .filter((check) => check.shouldRemove) + .map((check) => check.connection); + + await Promise.all( + connectionsToRemove.map((connection) => + oneIdentity.removeConnectionBetweenPersonAndProposal( + connection.UID_ESet, + connection.UID_Person + ) + ) + ); +} diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts new file mode 100644 index 00000000..daae36a4 --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.spec.ts @@ -0,0 +1,687 @@ +jest.mock('@user-office-software/duo-logger'); +jest.mock('../utils/ESSOneIdentity', () => ({ + ESSOneIdentity: jest.fn().mockImplementation(() => mockOneIdentity), +})); + +const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = '45'; + +jest.mock('process', () => ({ + env: { + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS, + }, +})); + +import { logger } from '@user-office-software/duo-logger'; + +import { syncVisitToOneIdentityHandler } from './syncVisitToOneIdentityHandler'; +import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { UID_ESet } from '../utils/interfaces/Eset'; +import { IdentityType, Person } from '../utils/interfaces/Person'; +import { + OrderState, + PersonWantsOrg, + PersonWantsOrgRole, +} from '../utils/interfaces/PersonWantsOrg'; +import { VisitMessage } from '../utils/interfaces/VisitMessage'; + +const mockOneIdentity: jest.Mocked> = { + login: jest.fn(), + logout: jest.fn(), + getPerson: jest.fn(), + getPersons: jest.fn(), + getPersonWantsOrg: jest.fn(), + getProposal: jest.fn(), + createProposal: jest.fn(), + connectPersonToProposal: jest.fn(), + getProposalPersonConnections: jest.fn(), + removeConnectionBetweenPersonAndProposal: jest.fn(), + createPersonWantsOrg: jest.fn(), + cancelPersonWantsOrg: jest.fn(), + hasPersonSiteAccessToProposal: jest.fn(), +}; + +const mockUidESet: UID_ESet = 'eset-uid-123'; + +const visitMessage: VisitMessage = { + visitorId: 'visitor-oidc-sub', + startAt: '2023-01-01T00:00:00.000Z', + endAt: '2023-01-10T00:00:00.000Z', + proposal: { + shortCode: 'proposal-short-code', + members: [{ oidcSub: 'member-oidc-sub' }], + dataAccessUsers: [{ oidcSub: 'visitor-oidc-sub' }], // Visitor is also a data access user + } as ProposalMessageData, +}; + +const visitMessageVisitorNotMember: VisitMessage = { + ...visitMessage, + proposal: { + ...visitMessage.proposal, + dataAccessUsers: [], // remove visitor from data access users + } as ProposalMessageData, +}; + +const visitMessageVisitorOnlyInVisitors: VisitMessage = { + ...visitMessage, + proposal: { + ...visitMessage.proposal, + dataAccessUsers: [], + visitors: [ + { + id: 524, + firstName: 'Visitor', + lastName: 'Only', + email: 'visitor@example.com', + oidcSub: 'visitor-oidc-sub', + }, + ], + } as ProposalMessageData, +}; + +describe('syncVisitToOneIdentityHandler', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Science user verification', () => { + it('should skip processing if visitor is not a science user', async () => { + // Mock person that is not a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: 'EMPLOYEEDK', + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is not a Science User, skipping', + {} + ); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); + + describe('VISIT_CREATED', () => { + it('should create site access and system access in One Identity for science users and connect to proposal', async () => { + // Mock the current time to a fixed value for testing + const mockNowDate = new Date('2022-12-15T00:00:00.000Z'); + const originalDateNow = Date.now; + Date.now = jest.fn(() => mockNowDate.getTime()); + + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + // Mock site access and system access creation responses + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + } as PersonWantsOrg; + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + } as PersonWantsOrg; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([]); // No existing connection + + // Mock sequential calls to createPersonWantsOrg with different responses + mockOneIdentity.createPersonWantsOrg + .mockResolvedValueOnce([mockSiteAccess]) + .mockResolvedValueOnce([mockSystemAccess]); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.getProposalPersonConnections).toHaveBeenCalledWith( + mockUidESet + ); + + // Verify site access creation + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenCalledTimes(2); + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + 1, + PersonWantsOrgRole.SITE_ACCESS, + visitMessage.visitorId, + visitMessage.startAt, + visitMessage.endAt, + visitMessage.proposal.shortCode + ); + + // Calculate expected system access dates + // validFrom should be the current mock date + const expectedValidFrom = mockNowDate.toISOString(); + const expectedEndDate = new Date(visitMessage.endAt); + expectedEndDate.setDate( + expectedEndDate.getDate() + + parseInt(ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS) + ); + + // Verify system access creation + expect(mockOneIdentity.createPersonWantsOrg).toHaveBeenNthCalledWith( + 2, + PersonWantsOrgRole.SYSTEM_ACCESS, + visitMessage.visitorId, + expectedValidFrom, + expectedEndDate.toISOString(), + 'site-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access created in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'System access created in One Identity', + { + UID_PersonWantsOrg: 'system-access-uid', + } + ); + + expect(mockOneIdentity.connectPersonToProposal).toHaveBeenCalledWith( + mockUidESet, + mockPerson.UID_Person + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection created between proposal and visitor', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + + expect(mockOneIdentity.logout).toHaveBeenCalled(); + + // Restore original Date.now + Date.now = originalDateNow; + }); + + it('should skip creating proposal connection if it already exists', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockSiteAccess = { + UID_PersonWantsOrg: 'site-access-uid', + } as PersonWantsOrg; + const mockSystemAccess = { + UID_PersonWantsOrg: 'system-access-uid', + } as PersonWantsOrg; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getProposalPersonConnections.mockResolvedValueOnce([ + { UID_Person: mockPerson.UID_Person, UID_ESet: mockUidESet }, + ]); // Connection exists + mockOneIdentity.createPersonWantsOrg + .mockResolvedValueOnce([mockSiteAccess]) + .mockResolvedValueOnce([mockSystemAccess]); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED); + + expect(mockOneIdentity.connectPersonToProposal).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection already exists, skipping', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error if proposal is not found in One Identity', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); // Proposal not found + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED) + ).rejects.toThrow( + 'Proposal not found in One Identity, cannot sync visit' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error if site access creation fails', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.createPersonWantsOrg.mockRejectedValueOnce( + new Error('Failed to create site access') + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_CREATED) + ).rejects.toThrow('Failed to create site access'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error when provided with an invalid date', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + + // Create a message with an invalid date + const invalidVisitMessage: VisitMessage = { + ...visitMessage, + startAt: 'invalid-date', + }; + + await expect( + syncVisitToOneIdentityHandler(invalidVisitMessage, Event.VISIT_CREATED) + ).rejects.toThrow('Invalid date provided to toIsoString: invalid-date'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.createPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); + + describe('VISITOR_DELETED', () => { + it('should remove visitor access and proposal connection in One Identity for science users (if not a member)', async () => { + // Mock person that is a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'proposal-short-code', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await syncVisitToOneIdentityHandler( + visitMessageVisitorNotMember, // Visitor is NOT a member + Event.VISIT_DELETED + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + visitMessageVisitorNotMember.visitorId + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessageVisitorNotMember.proposal + ); + expect(mockOneIdentity.getPersonWantsOrg).toHaveBeenCalledWith( + 'visitor-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'One Identity successfully logged in', + {} + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenNthCalledWith( + 1, + 'site-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access cancelled in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenNthCalledWith( + 2, + 'system-access-uid' + ); + + expect(logger.logInfo).toHaveBeenCalledWith( + 'System access cancelled in One Identity', + { + UID_PersonWantsOrg: 'system-access-uid', + } + ); + + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledTimes(2); + + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).toHaveBeenCalledWith(mockUidESet, mockPerson.UID_Person); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Connection removed between proposal and visitor', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + + expect(mockOneIdentity.logout).toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'One Identity successfully logged out', + {} + ); + }); + + it('should skip removing proposal connection if visitor is a member of the proposal', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'proposal-short-code', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + // Using original visitMessage where visitor IS a member + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is a proposal member, skipping removal', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should skip removing proposal connection if visitor is still in the visitors list', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'proposal-short-code', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await syncVisitToOneIdentityHandler( + visitMessageVisitorOnlyInVisitors, + Event.VISIT_DELETED + ); + + expect( + mockOneIdentity.removeConnectionBetweenPersonAndProposal + ).not.toHaveBeenCalled(); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is a proposal member, skipping removal', + { + uidPerson: mockPerson.UID_Person, + uidESet: mockUidESet, + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw an error if proposal is not found in One Identity on delete', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(undefined); // Proposal not found + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'Proposal not found in One Identity, cannot sync visit' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getProposal).toHaveBeenCalledWith( + visitMessage.proposal + ); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should skip processing if visitor is not a science user', async () => { + // Mock person that is not a science user + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: 'EMPLOYEEDK', + } as Person; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + + await syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Visitor is not a Science User, skipping', + {} + ); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if person not found', async () => { + mockOneIdentity.getPerson.mockResolvedValueOnce(undefined); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow('Person not found in One Identity'); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.getPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.cancelPersonWantsOrg).not.toHaveBeenCalled(); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if site access not found', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + const mockPersonWantsOrgs = [ + // No site access matching the dates + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-02T00:00:00.000Z', // Different from message.startAt + ValidUntil: '2023-01-10T00:00:00.000Z', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'Site access not found in One Identity, cannot remove access' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + + it('should throw error if system access not found', async () => { + const mockPerson = { + UID_Person: 'visitor-uid', + CCC_EmployeeSubType: IdentityType.ESSSCIENCEUSER, + } as Person; + + const mockPersonWantsOrgs = [ + { + UID_PersonWantsOrg: 'site-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: visitMessage.startAt, + ValidUntil: visitMessage.endAt, + CustomProperty04: 'proposal-short-code', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + // No system access with CustomProperty04 matching site-access-uid + { + UID_PersonWantsOrg: 'system-access-uid', + UID_PersonOrdered: 'visitor-uid', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01T00:00:00.000Z', + ValidUntil: '2023-01-10T00:00:00.000Z', + CustomProperty04: 'different-site-access-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + ]; + + mockOneIdentity.getPerson.mockResolvedValueOnce(mockPerson); + mockOneIdentity.getProposal.mockResolvedValueOnce(mockUidESet); + mockOneIdentity.getPersonWantsOrg.mockResolvedValueOnce( + mockPersonWantsOrgs + ); + + await expect( + syncVisitToOneIdentityHandler(visitMessage, Event.VISIT_DELETED) + ).rejects.toThrow( + 'System access not found in One Identity, cannot remove access' + ); + + expect(mockOneIdentity.login).toHaveBeenCalled(); + expect(mockOneIdentity.getPerson).toHaveBeenCalledWith( + 'visitor-oidc-sub' + ); + expect(mockOneIdentity.cancelPersonWantsOrg).toHaveBeenCalledWith( + 'site-access-uid' + ); + expect(logger.logInfo).toHaveBeenCalledWith( + 'Site access cancelled in One Identity', + { + UID_PersonWantsOrg: 'site-access-uid', + } + ); + expect(mockOneIdentity.logout).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts new file mode 100644 index 00000000..1b58ac96 --- /dev/null +++ b/src/queue/consumers/oneidentity/consumerCallbacks/syncVisitToOneIdentityHandler.ts @@ -0,0 +1,246 @@ +import process from 'process'; + +import { logger } from '@user-office-software/duo-logger'; + +import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { collectUsersFromProposalMessage } from '../../utils/collectUsersFromProposalMessage'; +import { ESSOneIdentity } from '../utils/ESSOneIdentity'; +import { IdentityType, UID_Person } from '../utils/interfaces/Person'; +import { + OrderState, + PersonWantsOrgRole, +} from '../utils/interfaces/PersonWantsOrg'; +import { VisitMessage } from '../utils/interfaces/VisitMessage'; + +const ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS = parseInt( + process.env.ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS || '30' +); + +export async function syncVisitToOneIdentityHandler( + { startAt, endAt, visitorId: oidcSub, proposal }: VisitMessage, + type: Event +): Promise { + const oneIdentity = new ESSOneIdentity(); + await oneIdentity.login(); + + logger.logInfo('One Identity successfully logged in', {}); + + try { + // Only Science Users' access should be managed! + const uidPerson = await getScienceUser(oneIdentity, oidcSub); + if (!uidPerson) { + logger.logInfo('Visitor is not a Science User, skipping', {}); + + return; + } + + const uidESet = await oneIdentity.getProposal(proposal); + + if (!uidESet) { + throw new Error('Proposal not found in One Identity, cannot sync visit'); + } + + if (type === Event.VISIT_CREATED) { + await createAccessInOneIdentity( + oneIdentity, + startAt, + endAt, + oidcSub, + proposal + ); + + // Every visitor should have access to the proposal folders + await createProposalConnection(oneIdentity, uidESet, uidPerson); + } else if (type === Event.VISIT_DELETED) { + await removeAccessFromOneIdentity(oneIdentity, startAt, endAt, uidPerson); + + // Remove the connection between the proposal and the visitor + await removeProposalConnection( + oneIdentity, + uidESet, + uidPerson, + oidcSub, + proposal + ); + } + } finally { + await oneIdentity.logout(); + logger.logInfo('One Identity successfully logged out', {}); + } +} + +// Find person UID from oidcSub +// If the person is not a science user, return undefined +async function getScienceUser( + oneIdentity: ESSOneIdentity, + centralAccount: string +): Promise { + // Find person UID from oidcSub + const person = await oneIdentity.getPerson(centralAccount); + + if (!person) { + throw new Error('Person not found in One Identity'); + } + + if (person.CCC_EmployeeSubType === IdentityType.ESSSCIENCEUSER) + return person.UID_Person; + else return undefined; +} + +async function createAccessInOneIdentity( + oneIdentity: ESSOneIdentity, + startAt: string, + endAt: string, + centralAccount: string, + proposal: ProposalMessageData +) { + // Create site access + const [pwoSite] = await oneIdentity.createPersonWantsOrg( + PersonWantsOrgRole.SITE_ACCESS, + centralAccount, + toIsoString(startAt), + toIsoString(endAt), + proposal.shortCode // CustomProperty04 - We store the proposal short code for the site access to be able to find it later + ); + + logger.logInfo('Site access created in One Identity', { + UID_PersonWantsOrg: pwoSite.UID_PersonWantsOrg, + }); + + // validFrom in One Identity should be in the future so that the access is not immediately available + const validFrom = Date.now(); + const validUntil = new Date(endAt).setDate( + new Date(endAt).getDate() + ONE_IDENTITY_SYSTEM_ACCESS_LASTS_FOR_DAYS + ); + + // Create system access + const [pwoSystem] = await oneIdentity.createPersonWantsOrg( + PersonWantsOrgRole.SYSTEM_ACCESS, + centralAccount, + toIsoString(validFrom), + toIsoString(validUntil), + pwoSite.UID_PersonWantsOrg // CustomProperty04 - We store the site access UID for the system access to be able to find it later + ); + + logger.logInfo('System access created in One Identity', { + UID_PersonWantsOrg: pwoSystem.UID_PersonWantsOrg, + }); +} + +async function removeAccessFromOneIdentity( + oneIdentity: ESSOneIdentity, + startAt: string, + endAt: string, + uidPerson: UID_Person +) { + // Find person wants orgs for the visitor + const personWantsOrgs = await oneIdentity.getPersonWantsOrg(uidPerson); + + // Find site access for the visitor + const siteAccess = personWantsOrgs.find( + (pwo) => + pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && + toIsoString(pwo.ValidFrom) === toIsoString(startAt) && + toIsoString(pwo.ValidUntil) === toIsoString(endAt) && + pwo.OrderState !== OrderState.ABORTED + ); + + if (!siteAccess) { + throw new Error( + 'Site access not found in One Identity, cannot remove access' + ); + } + + await oneIdentity.cancelPersonWantsOrg(siteAccess.UID_PersonWantsOrg); + + logger.logInfo('Site access cancelled in One Identity', { + UID_PersonWantsOrg: siteAccess.UID_PersonWantsOrg, + }); + + // Find system access for the site access (CustomProperty04 is the site access UID) + const systemAccess = personWantsOrgs.find( + (pwo) => + pwo.CustomProperty04 === siteAccess.UID_PersonWantsOrg && + pwo.DisplayOrg === PersonWantsOrgRole.SYSTEM_ACCESS && + pwo.OrderState !== OrderState.UNSUBSCRIBED + ); + + if (!systemAccess) { + throw new Error( + 'System access not found in One Identity, cannot remove access' + ); + } + + await oneIdentity.cancelPersonWantsOrg(systemAccess.UID_PersonWantsOrg); + + logger.logInfo('System access cancelled in One Identity', { + UID_PersonWantsOrg: systemAccess.UID_PersonWantsOrg, + }); +} + +async function createProposalConnection( + oneIdentity: ESSOneIdentity, + uidESet: string, + uidPerson: string +) { + // Check if the connection already exists + // If connection already exists, no need to create it again, reasons could be: + // - The visitor is a member of the proposal + // - The visitor has been added to the proposal in the past + const exists = (await oneIdentity.getProposalPersonConnections(uidESet)).some( + (c) => c.UID_Person === uidPerson + ); + + if (exists) { + logger.logInfo('Connection already exists, skipping', { + uidPerson, + uidESet, + }); + } else { + await oneIdentity.connectPersonToProposal(uidESet, uidPerson); + logger.logInfo('Connection created between proposal and visitor', { + uidPerson, + uidESet, + }); + } +} + +async function removeProposalConnection( + oneIdentity: ESSOneIdentity, + uidESet: string, + uidPerson: string, + oidcSub: string, + proposal: ProposalMessageData +) { + const isMember = collectUsersFromProposalMessage(proposal).some( + (m) => m.oidcSub === oidcSub + ); + + if (isMember) { + // [proposer, members, dataAccessUsers, visitors] should always have access to the proposal + logger.logInfo('Visitor is a proposal member, skipping removal', { + uidPerson, + uidESet, + }); + } else { + await oneIdentity.removeConnectionBetweenPersonAndProposal( + uidESet, + uidPerson + ); + logger.logInfo('Connection removed between proposal and visitor', { + uidPerson, + uidESet, + }); + } +} + +function toIsoString(date: string | number) { + const parsedDate = new Date(date); + + if (isNaN(parsedDate.getTime())) { + throw new Error(`Invalid date provided to toIsoString: ${date}`); + } + + return parsedDate.toISOString(); +} diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts new file mode 100644 index 00000000..3a037a22 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.spec.ts @@ -0,0 +1,621 @@ +jest.mock('./OneIdentityApi', () => ({ + OneIdentityApi: jest.fn().mockImplementation(() => mockOneIdentityApi), +})); +jest.mock('process', () => ({ + env: { + ONE_IDENTITY_API_USER: 'API_USER', + ONE_IDENTITY_API_PASSWORD: 'API_PASSWORD', + ONE_IDENTITY_PROPOSAL_IDENT_ESET_TYPE: 'PROPOSAL_IDENT_ESET_TYPE', + }, +})); + +import { ESSOneIdentity } from './ESSOneIdentity'; +import { + PersonWantsOrg, + PersonWantsOrgRole, + OrderState, +} from './interfaces/PersonWantsOrg'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; + +const mockOneIdentityApi = { + login: jest.fn(), + logout: jest.fn(), + createEntity: jest.fn(), + getEntities: jest.fn(), + deleteEntity: jest.fn(), + callScript: jest.fn(), +}; + +describe('ESSOneIdentity', () => { + let essOneIdentity: ESSOneIdentity; + + beforeEach(() => { + essOneIdentity = new ESSOneIdentity(); + }); + + describe('login', () => { + it('should login', async () => { + await essOneIdentity.login(); + expect(mockOneIdentityApi.login).toHaveBeenCalledWith( + 'API_USER', + 'API_PASSWORD' + ); + }); + }); + + describe('logout', () => { + it('should logout', async () => { + await essOneIdentity.logout(); + expect(mockOneIdentityApi.logout).toHaveBeenCalled(); + }); + }); + + describe('createProposal', () => { + it('should create proposal', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + UID_ESetType: 'eset-type-uid', + }, + }, + ]); + + mockOneIdentityApi.createEntity.mockResolvedValueOnce({ + uid: 'created-uid', + }); + + const result = await essOneIdentity.createProposal(proposalMessage); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'EsetType', + "Ident_ESetType='PROPOSAL_IDENT_ESET_TYPE'" + ); + expect(mockOneIdentityApi.createEntity).toHaveBeenCalledWith('ESET', { + DisplayName: 'some-short-code', + Ident_ESet: 'some-short-code', + UID_ESetType: 'eset-type-uid', + }); + expect(result).toBe('created-uid'); + }); + + it('should throw an error when UID_ESetType is not found', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + + await expect( + essOneIdentity.createProposal(proposalMessage) + ).rejects.toThrow('UID_ESetType not found: PROPOSAL_IDENT_ESET_TYPE'); + }); + }); + + describe('getProposal', () => { + it('should get proposal', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + UID_ESet: 'proposal-uid', + }, + }, + ]); + + const result = await essOneIdentity.getProposal(proposalMessage); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'ESET', + "Ident_ESet='some-short-code'" + ); + expect(result).toBe('proposal-uid'); + }); + + it('should return undefined if proposal is not found', async () => { + const proposalMessage = { + shortCode: 'some-short-code', + } as ProposalMessageData; + + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); + + const result = await essOneIdentity.getProposal(proposalMessage); + + expect(result).toBeUndefined(); + }); + }); + + describe('getPerson', () => { + it('should get a person', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + UID_Person: 'person-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }, + }, + ]); + + const result = await essOneIdentity.getPerson('0000-0000-0000-0000'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'Person', + "CentralAccount='0000-0000-0000-0000'", + ['CCC_EmployeeSubType'] + ); + expect(result).toEqual({ + UID_Person: 'person-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }); + }); + + // The CentralAccount is unique, but the response is an array of entities + it('should return the first person if multiple persons are found', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + UID_Person: 'person-1-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }, + }, + { + values: { + UID_Person: 'person-2-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }, + }, + ]); + + const result = await essOneIdentity.getPerson('0000-0000-0000-0000'); + + expect(result).toEqual({ + UID_Person: 'person-1-uid', + CCC_EmployeeSubType: 'ESSSCIENCEUSER', + }); + }); + }); + + describe('getPersons', () => { + it('should get person records for multiple users, undefined if not found', async () => { + mockOneIdentityApi.getEntities.mockImplementation((table, filter) => { + if ( + table === 'Person' && + filter === "CentralAccount='unknown-oidc-sub'" + ) + return Promise.resolve([]); + else + return Promise.resolve([ + { + values: { + UID_Person: 'known-person-uid', + }, + }, + ]); + }); + + const result = await essOneIdentity.getPersons([ + 'unknown-oidc-sub', + 'known-oidc-sub', + ]); + + expect(result).toEqual(['known-person-uid']); + }); + }); + + describe('connectPersonToProposal', () => { + it('should connect person to proposal', async () => { + mockOneIdentityApi.createEntity.mockResolvedValueOnce({ + uid: 'created-uid', + }); + + const result = await essOneIdentity.connectPersonToProposal( + 'proposal-uid', + 'person-uid' + ); + + expect(mockOneIdentityApi.createEntity).toHaveBeenCalledWith( + 'PersonHasESET', + { + UID_ESet: 'proposal-uid', + UID_Person: 'person-uid', + } + ); + expect(result).toBe('created-uid'); + }); + }); + + describe('removeConnectionBetweenPersonAndProposal', () => { + it('should remove connection between person and proposal', async () => { + await essOneIdentity.removeConnectionBetweenPersonAndProposal( + 'proposal-uid', + 'person-uid' + ); + + expect(mockOneIdentityApi.deleteEntity).toHaveBeenCalledWith( + 'PersonHasESET', + 'proposal-uid;person-uid' + ); + }); + }); + + describe('getProposalPersonConnections', () => { + it('should get proposal person connections', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + UID_ESet: 'proposal-uid', + UID_Person: 'person-1-uid', + }, + }, + { + values: { + UID_ESet: 'proposal-uid', + UID_Person: 'person-2-uid', + }, + }, + ]); + + const result = + await essOneIdentity.getProposalPersonConnections('proposal-uid'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonHasESET', + "UID_ESet='proposal-uid'" + ); + expect(result).toEqual([ + { + UID_ESet: 'proposal-uid', + UID_Person: 'person-1-uid', + }, + { + UID_ESet: 'proposal-uid', + UID_Person: 'person-2-uid', + }, + ]); + }); + }); + + describe('createPersonWantsOrg', () => { + const role = PersonWantsOrgRole.SITE_ACCESS; + const centralAccount = 'user123'; + const startDate = '2023-01-01'; + const endDate = '2023-12-31'; + const mockPersonWantsOrgData: PersonWantsOrg[] = [ + { + UID_PersonWantsOrg: 'pwo-123', + ValidFrom: startDate, + ValidUntil: endDate, + } as PersonWantsOrg, + ]; + + it('should successfully create site access', async () => { + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Data: mockPersonWantsOrgData, + Message: 'Success', + }); + + const result = await essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate + ); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [role, centralAccount, centralAccount, startDate, endDate, '', ''] + ); + expect(result).toEqual(mockPersonWantsOrgData); + }); + + it('should successfully create site access with custom data', async () => { + const customData = 'custom-data-123'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Data: mockPersonWantsOrgData, + Message: 'Success', + }); + + const result = await essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate, + customData + ); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [ + role, + centralAccount, + centralAccount, + startDate, + endDate, + customData, + '', + ] + ); + expect(result).toEqual(mockPersonWantsOrgData); + }); + + it('should throw an error when site access creation fails', async () => { + const errorMessage = 'Access denied'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: false, + Data: null, + Message: errorMessage, + }); + + await expect( + essOneIdentity.createPersonWantsOrg( + role, + centralAccount, + startDate, + endDate + ) + ).rejects.toThrow(`Failed to create site access: ${errorMessage}`); + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccess', + [role, centralAccount, centralAccount, startDate, endDate, '', ''] + ); + }); + }); + + describe('cancelPersonWantsOrg', () => { + const uidPersonWantsOrg = 'pwo-123'; + + it('should successfully cancel site access', async () => { + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: true, + Message: 'Success', + }); + + await essOneIdentity.cancelPersonWantsOrg(uidPersonWantsOrg); + + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + }); + + it('should throw an error when site access cancellation fails', async () => { + const errorMessage = 'Access not found'; + mockOneIdentityApi.callScript.mockResolvedValueOnce({ + IsSuccess: false, + Message: errorMessage, + }); + + await expect( + essOneIdentity.cancelPersonWantsOrg(uidPersonWantsOrg) + ).rejects.toThrow(`Failed to cancel site access:${errorMessage}`); + expect(mockOneIdentityApi.callScript).toHaveBeenCalledWith( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + }); + }); + + describe('getPersonWantsOrg', () => { + const mockPersonWantsOrgData = [ + { + values: { + UID_PersonWantsOrg: 'pwo-123', + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + ValidFrom: '2023-01-01', + ValidUntil: '2023-12-31', + OrderState: 'Granted', + }, + }, + { + values: { + UID_PersonWantsOrg: 'pwo-456', + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, + ValidFrom: '2023-01-01', + ValidUntil: '2023-12-31', + OrderState: 'Granted', + }, + }, + ]; + + it('should get person wants org records with default parameters', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce( + mockPersonWantsOrgData + ); + + const result = await essOneIdentity.getPersonWantsOrg('person-uid'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access' OR DisplayOrg='Experiment visit - system access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([ + mockPersonWantsOrgData[0].values, + mockPersonWantsOrgData[1].values, + ]); + }); + + it('should get person wants org records with custom parameters', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce( + mockPersonWantsOrgData + ); + + const result = await essOneIdentity.getPersonWantsOrg('person-uid', [ + PersonWantsOrgRole.SITE_ACCESS, + ]); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([ + mockPersonWantsOrgData[0].values, + mockPersonWantsOrgData[1].values, + ]); + }); + + it('should return empty array when no records found', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); // No records + + const result = await essOneIdentity.getPersonWantsOrg('person-uid'); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + "UID_PersonOrdered='person-uid' AND (DisplayOrg='Experiment visit - site access' OR DisplayOrg='Experiment visit - system access')", + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toEqual([]); + }); + }); + + describe('hasPersonSiteAccessToProposal', () => { + const uidPerson = 'person-123'; + const proposalUid = 'proposal-abc'; + + it('should return true if person has site access to the proposal', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + + expect(mockOneIdentityApi.getEntities).toHaveBeenCalledWith( + 'PersonWantsOrg', + `UID_PersonOrdered='${uidPerson}' AND (DisplayOrg='${PersonWantsOrgRole.SITE_ACCESS}')`, + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + expect(result).toBe(true); + }); + + it('should return false if person does not have site access to the proposal (different proposal)', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: 'other-proposal-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if person does not have site access to the proposal (different role)', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SYSTEM_ACCESS, // Different role + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if site access is aborted', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.ABORTED, // Aborted state + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return false if no PersonWantsOrg records are found', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([]); // No records + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(false); + }); + + it('should return true if person has multiple site access records and one matches', async () => { + mockOneIdentityApi.getEntities.mockResolvedValueOnce([ + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: 'other-proposal-uid', + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + { + values: { + DisplayOrg: PersonWantsOrgRole.SITE_ACCESS, + CustomProperty04: proposalUid, + OrderState: OrderState.GRANTED, + } as PersonWantsOrg, + }, + ]); + + const result = await essOneIdentity.hasPersonSiteAccessToProposal( + uidPerson, + proposalUid + ); + expect(result).toBe(true); + }); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts new file mode 100644 index 00000000..ad79a121 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/ESSOneIdentity.ts @@ -0,0 +1,219 @@ +import { env } from 'process'; + +import { Eset, UID_ESet } from './interfaces/Eset'; +import { EsetType } from './interfaces/EsetType'; +import { Person, UID_Person } from './interfaces/Person'; +import { PersonHasESET } from './interfaces/PersonHasESET'; +import { + OrderState, + PersonWantsOrg, + PersonWantsOrgRole, +} from './interfaces/PersonWantsOrg'; +import { + SCProposalSiteAccessCancelResponse, + SCProposalSiteAccessResponse, +} from './interfaces/SCProposalSiteAccessResponse'; +import { OneIdentityApi } from './OneIdentityApi'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; + +export interface UserPersonConnection { + oidcSub: string; + uidPerson: UID_Person | undefined; +} + +const SERVER_URL = env.ONE_IDENTITY_APP_SERVER_URL || ''; +const API_USER = env.ONE_IDENTITY_API_USER || ''; +const API_PASSWORD = env.ONE_IDENTITY_API_PASSWORD || ''; +const PROPOSAL_IDENT_ESET_TYPE = + env.ONE_IDENTITY_PROPOSAL_IDENT_ESET_TYPE || ''; + +// ESSOneIdentity is a class that provides methods to interact with ESS One Identity Manager +// It is used to create, update and delete proposals and users in ESS One Identity Manager +export class ESSOneIdentity { + private oneIdentityApi: OneIdentityApi; + + constructor() { + this.oneIdentityApi = new OneIdentityApi(SERVER_URL); + } + + public async login() { + await this.oneIdentityApi.login(API_USER, API_PASSWORD); + } + + public async logout() { + await this.oneIdentityApi.logout(); + } + + public async createProposal( + proposalMessage: ProposalMessageData + ): Promise { + // get "Science Proposal" UID_ESetType from ESS One Identity + const entities = await this.oneIdentityApi.getEntities( + 'EsetType', + `Ident_ESetType='${PROPOSAL_IDENT_ESET_TYPE}'` + ); + + const uidESetType = entities[0]?.values?.UID_ESetType; + + if (!uidESetType) { + throw new Error('UID_ESetType not found: ' + PROPOSAL_IDENT_ESET_TYPE); + } + + // create proposal in ESS One Identity + const esetResponse = await this.oneIdentityApi.createEntity< + Omit + >('ESET', { + UID_ESetType: uidESetType, + Ident_ESet: proposalMessage.shortCode, + DisplayName: proposalMessage.shortCode, + }); + + return esetResponse.uid; + } + + public async getProposal( + proposalMessage: ProposalMessageData + ): Promise { + const entities = await this.oneIdentityApi.getEntities( + 'ESET', + `Ident_ESet='${proposalMessage.shortCode}'` + ); + + return entities[0]?.values?.UID_ESet; + } + + public async getPerson(centralAccount: string): Promise { + const entities = await this.oneIdentityApi.getEntities( + 'Person', + `CentralAccount='${centralAccount}'`, + ['CCC_EmployeeSubType'] + ); + + return entities[0]?.values; + } + + public async getPersons(centralAccounts: string[]): Promise { + return ( + await Promise.all( + centralAccounts.map( + async (centralAccount) => + (await this.getPerson(centralAccount))?.UID_Person + ) + ) + ).filter((uidPerson): uidPerson is string => uidPerson !== undefined); + } + + public async connectPersonToProposal( + uidEset: UID_ESet, + uidPerson: UID_Person + ): Promise { + const { uid } = await this.oneIdentityApi.createEntity( + 'PersonHasESET', + { + UID_ESet: uidEset, + UID_Person: uidPerson, + } + ); + + return uid; + } + + public async removeConnectionBetweenPersonAndProposal( + uidEset: UID_ESet, + uidPerson: UID_Person + ) { + await this.oneIdentityApi.deleteEntity( + 'PersonHasESET', + `${uidEset};${uidPerson}` + ); + } + + public async getProposalPersonConnections( + uidEset: UID_ESet + ): Promise { + const entities = await this.oneIdentityApi.getEntities( + 'PersonHasESET', + `UID_ESet='${uidEset}'` + ); + + return entities.map(({ values }) => values); + } + + public async createPersonWantsOrg( + role: PersonWantsOrgRole, + centralAccount: string, + startDate: string, + endDate: string, + customData: string = '' + ): Promise { + const res = + await this.oneIdentityApi.callScript( + 'SCProposalSiteAccess', + [ + role, // access type + centralAccount, // requester + centralAccount, // recipient + startDate, + endDate, + customData, // PersonWantsOrg.CustomProperty04 + '', // UID_PersonWantsOrg (empty for new) + ] + ); + + if (!res.IsSuccess) + throw new Error('Failed to create site access: ' + res.Message); + + return res.Data; + } + + public async cancelPersonWantsOrg(uidPersonWantsOrg: string): Promise { + const res = + await this.oneIdentityApi.callScript( + 'SCProposalSiteAccessCancel', + [uidPersonWantsOrg] + ); + + if (!res.IsSuccess) + throw new Error('Failed to cancel site access:' + res.Message); + } + + public async getPersonWantsOrg( + uidPerson: UID_Person, + displayOrgs: PersonWantsOrgRole[] = [ + PersonWantsOrgRole.SITE_ACCESS, + PersonWantsOrgRole.SYSTEM_ACCESS, + ] + ): Promise { + const entities = await this.oneIdentityApi.getEntities( + 'PersonWantsOrg', + `UID_PersonOrdered='${uidPerson}' AND (${displayOrgs + .map((org) => `DisplayOrg='${org}'`) + .join(' OR ')})`, + [ + 'ValidFrom', + 'ValidUntil', + 'OrderState', + 'DisplayOrg', + 'CustomProperty04', + ] + ); + + return entities.map(({ values }) => values); + } + + public async hasPersonSiteAccessToProposal( + uidPerson: UID_Person, + proposal: UID_ESet + ): Promise { + const personWantsOrgs = await this.getPersonWantsOrg(uidPerson, [ + PersonWantsOrgRole.SITE_ACCESS, + ]); + + return personWantsOrgs.some( + (pwo) => + pwo.DisplayOrg === PersonWantsOrgRole.SITE_ACCESS && + pwo.CustomProperty04 === proposal && + pwo.OrderState !== OrderState.ABORTED + ); + } +} diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts new file mode 100644 index 00000000..ac6e232a --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.spec.ts @@ -0,0 +1,155 @@ +jest.mock('axios'); + +import axios from 'axios'; + +import { OneIdentityApi } from './OneIdentityApi'; + +describe('OneIdentityApi', () => { + let api: OneIdentityApi; + + beforeEach(() => { + (axios.create as jest.Mock).mockReturnValue(axios); + api = new OneIdentityApi('http://localhost'); + }); + + describe('login', () => { + it('should login successfully', async () => { + (axios.post as jest.Mock).mockResolvedValueOnce({ + headers: { 'set-cookie': 'test-cookie' }, + }); + + await api.login('user', 'password'); + + expect(axios.post).toHaveBeenCalledWith( + '/auth/apphost', + { + module: 'DialogUser', + props: [ + { + name: 'User', + type: 'Edit', + display: 'User', + value: 'user', + isMandatory: true, + }, + { + name: 'Password', + type: 'Password', + display: 'Password', + value: 'password', + isMandatory: false, + }, + ], + config: [], + }, + { + baseURL: 'http://localhost', + } + ); + }); + + it('should throw error when no cookie is set', async () => { + (axios.post as jest.Mock).mockResolvedValueOnce({ + headers: {}, + }); + + await expect(api.login('user', 'password')).rejects.toThrow( + 'No cookie set' + ); + }); + + it('should handle API errors during login', async () => { + const errorMessage = 'Authentication failed'; + (axios.post as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + + await expect(api.login('user', 'password')).rejects.toThrow(errorMessage); + }); + }); + + describe('logout', () => { + it('should logout successfully', async () => { + (axios.post as jest.Mock).mockResolvedValueOnce({}); + + await api.logout(); + + expect(axios.post).toHaveBeenCalledWith('/auth/logout', undefined, { + baseURL: 'http://localhost', + }); + }); + }); + + describe('createEntity', () => { + it('should create entity successfully', async () => { + const mockEntity = { id: 1, name: 'test' }; + (axios.post as jest.Mock).mockResolvedValueOnce({ data: mockEntity }); + + const result = await api.createEntity('testTable', mockEntity); + + expect(axios.post).toHaveBeenCalledWith('/entity/testTable', { + values: mockEntity, + }); + expect(result).toEqual(mockEntity); + }); + }); + + describe('getEntities', () => { + it('should get entities successfully', async () => { + const mockEntities = [{ id: 1, name: 'test' }]; + (axios.get as jest.Mock).mockResolvedValueOnce({ data: mockEntities }); + + const result = await api.getEntities('testTable', 'id=1'); + + expect(axios.get).toHaveBeenCalledWith('/entities/testTable', { + params: { where: 'id=1' }, + }); + expect(result).toEqual(mockEntities); + }); + + it('should get entities with display columns successfully', async () => { + const mockEntities = [{ id: 1, name: 'test' }]; + (axios.get as jest.Mock).mockResolvedValueOnce({ data: mockEntities }); + + const result = await api.getEntities('testTable', 'id=1', ['name']); + + expect(axios.get).toHaveBeenCalledWith('/entities/testTable', { + params: { where: 'id=1', displayColumns: 'name' }, + }); + expect(result).toEqual(mockEntities); + }); + }); + + describe('deleteEntity', () => { + it('should delete entity successfully', async () => { + (axios.delete as jest.Mock).mockResolvedValueOnce({}); + + await api.deleteEntity('testTable', '1'); + + expect(axios.delete).toHaveBeenCalledWith('/entity/testTable/1'); + }); + }); + + describe('callScript', () => { + it('should call script successfully', async () => { + const mockResult = { success: true, data: 'script result' }; + (axios.put as jest.Mock).mockResolvedValueOnce({ data: mockResult }); + + const scriptParams = ['param1', 'param2']; + const result = await api.callScript('TestScript', scriptParams); + + expect(axios.put).toHaveBeenCalledWith('/script/TestScript', { + parameters: scriptParams, + returnRawResult: true, + }); + expect(result).toEqual(mockResult); + }); + + it('should handle errors during script calls', async () => { + const errorMessage = 'Script execution failed'; + (axios.put as jest.Mock).mockRejectedValueOnce(new Error(errorMessage)); + + await expect(api.callScript('TestScript', [])).rejects.toThrow( + errorMessage + ); + }); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts new file mode 100644 index 00000000..20c2045b --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/OneIdentityApi.ts @@ -0,0 +1,125 @@ +import axios, { AxiosInstance, AxiosResponse } from 'axios'; + +interface CreateEntityResult { + uid?: string; + uri?: string; +} + +interface EntityValues { + values: T; +} + +// This class is a basic wrapper around the OneIdentity API +export class OneIdentityApi { + private baseUrl: string; + private apiUrl: string; + private axiosInstance: AxiosInstance; + + constructor(url: string) { + this.baseUrl = url; + this.apiUrl = `${this.baseUrl}/api`; + this.axiosInstance = this.createAxiosInstance(); + } + + private createAxiosInstance(): AxiosInstance { + return axios.create({ + baseURL: this.apiUrl, + withCredentials: true, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Content-Type': 'application/json', + }, + }); + } + + public async login(user: string, password: string): Promise { + const res = await this.axiosInstance.post( + '/auth/apphost', + { + module: 'DialogUser', + props: [ + { + name: 'User', + type: 'Edit', + display: 'User', + value: user, + isMandatory: true, + }, + { + name: 'Password', + type: 'Password', + display: 'Password', + value: password, + isMandatory: false, + }, + ], + config: [], + }, + { + baseURL: this.baseUrl, + } + ); + + if (!res.headers['set-cookie']) throw new Error('No cookie set'); + + // We need to set the cookie for the following requests + // It contains the session information + this.axiosInstance.defaults.headers.Cookie = res.headers['set-cookie']; + } + + public logout(): Promise { + return this.axiosInstance.post('/auth/logout', undefined, { + baseURL: this.baseUrl, + }); + } + + public async createEntity( + table: string, + values: T + ): Promise { + const { data } = await this.axiosInstance.post< + T, + AxiosResponse + >(`/entity/${table}`, { + values, + }); + + return data; + } + + public async getEntities( + table: string, + where: string, + displayColumns?: (keyof T)[] + ): Promise[]> { + const { data } = await this.axiosInstance.get< + T, + AxiosResponse[]> + >(`/entities/${table}`, { + params: { + where, + ...(displayColumns && displayColumns.length > 0 + ? { displayColumns: displayColumns.join(',') } + : {}), + }, + }); + + return data; + } + + public deleteEntity(table: string, uid: string): Promise { + return this.axiosInstance.delete(`/entity/${table}/${uid}`); + } + + public async callScript(name: string, parameters: string[]): Promise { + const { data } = await this.axiosInstance.put>( + `/script/${name}`, + { + parameters, + returnRawResult: true, // One Identity API returns the result as a string from the script. Axios will be able to JSON parse it. + } + ); + + return data; + } +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts b/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts new file mode 100644 index 00000000..81e2b3a6 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/Eset.ts @@ -0,0 +1,10 @@ +import { UID_ESetType } from './EsetType'; + +export type UID_ESet = string; + +export interface Eset { + UID_ESet: UID_ESet; + UID_ESetType: UID_ESetType; + Ident_ESet: string; + DisplayName: string; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts b/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts new file mode 100644 index 00000000..83113e56 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/EsetType.ts @@ -0,0 +1,6 @@ +export type UID_ESetType = string; + +export interface EsetType { + Ident_ESetType: string; + UID_ESetType: UID_ESetType; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/Person.ts b/src/queue/consumers/oneidentity/utils/interfaces/Person.ts new file mode 100644 index 00000000..a5dfc121 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/Person.ts @@ -0,0 +1,13 @@ +export enum IdentityType { + ESSSCIENCEUSER = 'ESSSCIENCEUSER', + EMPLOYEEDK = 'EMPLOYEEDK', +} + +export type UID_Person = string; + +export interface Person { + CCC_EmployeeSubType: IdentityType; + CentralAccount: string; + InternalName: string; + UID_Person: UID_Person; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts b/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts new file mode 100644 index 00000000..a307c90a --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/PersonHasESET.ts @@ -0,0 +1,7 @@ +import { UID_ESet } from './Eset'; +import { UID_Person } from './Person'; + +export interface PersonHasESET { + UID_Person: UID_Person; + UID_ESet: UID_ESet; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts b/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts new file mode 100644 index 00000000..47cd44df --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/PersonWantsOrg.ts @@ -0,0 +1,93 @@ +export enum OrderState { + GRANTED = 'Granted', + ABORTED = 'Aborted', + WAITING = 'Waiting', + ASSIGNED = 'Assigned', + UNSUBSCRIBED = 'Unsubscribed', + ORDERPRODUCT = 'OrderProduct', +} + +export enum PersonWantsOrgRole { + SYSTEM_ACCESS = 'Experiment visit - system access', + SITE_ACCESS = 'Experiment visit - site access', +} + +export interface PersonWantsOrg { + AdditionalData: string; + CCC_CustomDate01: string; + CCC_CustomPerson01: string; + CheckResult: number; + CheckResultDetail: string; + CustomProperty01: string; + CustomProperty02: string; + CustomProperty03: string; + CustomProperty04: string; + CustomProperty05: string; + CustomProperty06: string; + CustomProperty07: string; + CustomProperty08: string; + CustomProperty09: string; + CustomProperty10: string; + DateActivated: string; + DateDeactivated: string; + DateHead: string; + DecisionLevel: number; + DisplayObjectKeyAssignment: string; + DisplayOrg: PersonWantsOrgRole; + DisplayOrgParent: string; + DisplayOrgParentOfParent: string; + DisplayPersonHead: string; + DisplayPersonInserted: string; + DisplayPersonOrdered: string; + GenProcID: string; + IsCrossFunctional: boolean; + IsOptionalChild: boolean; + IsOrderForWorkDesk: boolean; + IsReserved: boolean; + ObjectKeyAssignment: string; + ObjectKeyElementUsedInAssign: string; + ObjectKeyFinal: string; + ObjectKeyOrdered: string; + ObjectKeyOrgUsedInAssign: string; + OrderDate: string; + OrderDetail1: string; + OrderDetail2: string; + OrderReason: string; + OrderState: OrderState; + PeerGroupFactor: number; + PWOPriority: number; + Quantity: number; + ReasonHead: string; + Recommendation: number; + RecommendationDetail: string; + UID_Department: string; + UID_ITShopOrgFinal: string; + UID_Org: string; + UID_OrgParent: string; + UID_OrgParentOfParent: string; + UID_PersonHead: string; + UID_PersonInserted: string; + UID_PersonOrdered: string; + UID_PersonWantsOrg: string; + UID_PersonWantsOrgParent: string; + UID_ProfitCenter: string; + UID_PWOState: string; + UID_QERJustification: string; + UID_QERJustificationOrder: string; + UID_QERResourceType: string; + UID_QERWorkingMethod: string; + UID_ShoppingCartOrder: string; + UID_WorkDeskOrdered: string; + UiOrderState: string; + ValidFrom: string; + ValidUntil: string; + ValidUntilProlongation: string; + ValidUntilUnsubscribe: string; + XDateInserted: string; + XDateUpdated: string; + XMarkedForDeletion: number; + XObjectKey: string; + XTouched: string; + XUserInserted: string; + XUserUpdated: string; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts b/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts new file mode 100644 index 00000000..1ae848f2 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/SCProposalSiteAccessResponse.ts @@ -0,0 +1,14 @@ +import { PersonWantsOrg } from './PersonWantsOrg'; + +export interface SiteAccessResponse { + IsSuccess: boolean; + Message: string | null; +} + +export interface SCProposalSiteAccessResponse extends SiteAccessResponse { + Data: PersonWantsOrg[]; +} + +export interface SCProposalSiteAccessCancelResponse extends SiteAccessResponse { + Data: []; +} diff --git a/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts new file mode 100644 index 00000000..ce180475 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/interfaces/VisitMessage.ts @@ -0,0 +1,8 @@ +import { ProposalMessageData } from '../../../../../models/ProposalMessage'; + +export interface VisitMessage { + startAt: string; + endAt: string; + visitorId: string; + proposal: ProposalMessageData; +} diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts new file mode 100644 index 00000000..5f7c0d78 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.spec.ts @@ -0,0 +1,58 @@ +import { isVisitMessage } from './isVisitMessage'; + +describe('isVisitMessage', () => { + it('should return false if message is not an object', () => { + const message = 'not an object'; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if message is null', () => { + const message = null; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if visitorId is undefined', () => { + const message = { + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if startAt is undefined', () => { + const message = { + visitorId: 'visitor123', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if endAt is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return false if proposal is undefined', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + }; + expect(isVisitMessage(message)).toBe(false); + }); + + it('should return true if the message is valid', () => { + const message = { + visitorId: 'visitor123', + startAt: '2023-01-01T00:00:00Z', + endAt: '2023-01-02T00:00:00Z', + proposal: { + shortCode: 'proposal-short-code', + }, + }; + expect(isVisitMessage(message)).toBe(true); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/isVisitMessage.ts b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts new file mode 100644 index 00000000..ed7cb282 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/isVisitMessage.ts @@ -0,0 +1,12 @@ +import { VisitMessage } from './interfaces/VisitMessage'; + +export function isVisitMessage(message: any): message is VisitMessage { + return ( + message != null && + typeof message === 'object' && + 'visitorId' in message && + 'startAt' in message && + 'endAt' in message && + 'proposal' in message + ); +} diff --git a/src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts b/src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts new file mode 100644 index 00000000..eb26d670 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/validateProposalMessage.spec.ts @@ -0,0 +1,62 @@ +import { validateProposalMessage } from './validateProposalMessage'; + +describe('validateProposalMessage', () => { + it('should throw an error if message is not an object', () => { + const message = 'not an object'; + + expect(() => validateProposalMessage(message)).toThrow( + 'Invalid proposal message' + ); + }); + + it('should throw an error if message is null', () => { + const message = null; + + expect(() => validateProposalMessage(message)).toThrow( + 'Invalid proposal message' + ); + }); + + it('should throw an error if shortCode is undefined', () => { + const message = { + proposer: {}, + members: [], + }; + + expect(() => validateProposalMessage(message)).toThrow( + 'Invalid proposal message' + ); + }); + + it('should throw an error if proposer is undefined', () => { + const message = { + shortCode: 'shortCode', + members: [], + }; + + expect(() => validateProposalMessage(message)).toThrow( + 'Invalid proposal message' + ); + }); + + it('should throw an error if members is undefined', () => { + const message = { + shortCode: 'shortCode', + proposer: {}, + }; + + expect(() => validateProposalMessage(message)).toThrow( + 'Invalid proposal message' + ); + }); + + it('should return the message if shortCode, proposer, and members are defined', () => { + const message = { + shortCode: 'shortCode', + proposer: {}, + members: [], + }; + + expect(validateProposalMessage(message)).toEqual(message); + }); +}); diff --git a/src/queue/consumers/oneidentity/utils/validateProposalMessage.ts b/src/queue/consumers/oneidentity/utils/validateProposalMessage.ts new file mode 100644 index 00000000..67227867 --- /dev/null +++ b/src/queue/consumers/oneidentity/utils/validateProposalMessage.ts @@ -0,0 +1,16 @@ +import { ProposalMessageData } from '../../../../models/ProposalMessage'; + +// For OneIdentity, only these fields are required +export function validateProposalMessage( + message: any +): ProposalMessageData | never { + if ( + message?.shortCode === undefined || + message?.proposer === undefined || + message?.members === undefined + ) { + throw new Error('Invalid proposal message'); + } + + return message; +} diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts index 7c92e207..20d0f21f 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/createChatroom.ts @@ -12,7 +12,28 @@ function isValidUser(user: ProposalUser) { return user?.oidcSub && user?.firstName && user?.lastName && user?.email; } -function validateUsers(users: ProposalUser[]) { +async function checkUserInfo( + synapseService: SynapseService, + user: ProposalUser +) { + const userId = await synapseService.getUserId(user); + const userInfo = userId + ? await synapseService.getUserInfo(userId.user_id) + : null; + + if (!userInfo?.deactivated) { + return { isDeactivated: false, userExists: !!userId }; + } + + logger.logInfo('Deactivated user will not be invited to the chatroom ', { + user: user, + information: userInfo, + }); + + return { isDeactivated: true }; +} + +function validateUsersProfile(users: ProposalUser[]) { const validUsers = []; const invalidUsers = []; for (const user of users) { @@ -29,9 +50,15 @@ const createChatroom = async (message: ValidProposalMessageData) => { const synapseService: SynapseService = container.resolve( Tokens.SynapseService ); + await synapseService.login(); const allUsersOnProposal = [...message.members, message.proposer]; - const { validUsers, invalidUsers } = validateUsers(allUsersOnProposal); + const { validUsers, invalidUsers } = validateUsersProfile(allUsersOnProposal); + + // NOTE: activeUsers are users that are valid and not deactivated, + // deactivated users should not be invited to the chatroom. + const activeUsers = []; + if (invalidUsers.length > 0) { logger.logError( 'Some users will not be invited to the chatroom due to them being invalid', @@ -41,12 +68,18 @@ const createChatroom = async (message: ValidProposalMessageData) => { for (const user of validUsers) { try { - const userExists = await synapseService.userExists(user); + const { isDeactivated, userExists } = await checkUserInfo( + synapseService, + user + ); + if (!isDeactivated) { + activeUsers.push(user); - if (!userExists) { - await synapseService.createUser(user, defaultPassword); + if (!userExists) { + await synapseService.createUser(user, defaultPassword); + } + await synapseService.updateUser(user); } - await synapseService.updateUser(user); } catch (err: unknown) { logger.logError('Error while upserting chatroom user: ', { user, err }); } @@ -66,10 +99,11 @@ const createChatroom = async (message: ValidProposalMessageData) => { logger.logInfo('Room does not exist. Creating new room', { roomName: message.shortCode, }); + const result = await synapseService.createRoom( message.shortCode, message.title, - validUsers + activeUsers ); const room = await synapseService.getRoomByName(message.shortCode); @@ -78,12 +112,14 @@ const createChatroom = async (message: ValidProposalMessageData) => { room, }); } else { - const users = await synapseService.invite(rooms[0].room_id, validUsers); + const users = await synapseService.invite(rooms[0].room_id, activeUsers); const room = await synapseService.getRoomByName(message.shortCode); logger.logInfo('Users invited to existing room', { room, users }); } } catch (err: unknown) { logger.logException('Error while creating chatroom: ', err, { message }); + } finally { + await synapseService.logout(); } }; diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts index 6bbe2113..8c5d6f47 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.spec.ts @@ -7,6 +7,7 @@ jest.mock('node:process', () => ({ jest.mock('@user-office-software/duo-logger'); import { exec } from 'node:child_process'; +import { env } from 'node:process'; import { logger } from '@user-office-software/duo-logger'; @@ -16,9 +17,11 @@ import { ValidProposalMessageData } from '../../../utils/validateProposalMessage describe('proposalFoldersCreation', () => { const proposalMessage = { shortCode: 'shortCode', - instrument: { - shortCode: 'shortCode', - }, + instruments: [ + { + shortCode: 'shortCode', + }, + ], proposer: { email: 'test.proposer@email.com', }, @@ -67,4 +70,23 @@ describe('proposalFoldersCreation', () => { } ); }); + + it('should call exec with the correct command', () => { + env.PROPOSAL_FOLDERS_CREATION_COMMAND = + 'command '; + env.PROPOSAL_FOLDERS_CREATION_GROUP_PREFIX = 'group_prefix_'; + (exec as unknown as jest.Mock).mockImplementationOnce( + (command, callback) => { + callback(undefined, '', undefined); + } + ); + + proposalFoldersCreation(proposalMessage); + + expect(exec).toHaveBeenCalledTimes(1); + expect(exec).toHaveBeenCalledWith( + 'command shortcode 2026 shortCode group_prefix_shortCode test.proposer@email.com test.member@email.com', + expect.any(Function) + ); + }); }); diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.ts index 2f5eeb42..aee7ff1f 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/proposalFoldersCreation.ts @@ -10,53 +10,58 @@ const proposalFoldersCreation = async ( ) => { // prepare path with correct year, instrument, proposal const proposalId = proposalMessage.shortCode; - const group = proposalMessage.shortCode; + const group = `${env.PROPOSAL_FOLDERS_CREATION_GROUP_PREFIX}${proposalMessage.shortCode}`; const year = new Date().getFullYear().toString(); - const instrument = proposalMessage.instrument.shortCode.toLowerCase(); const proposerEmail = proposalMessage.proposer.email; const membersEmails = proposalMessage.members.map((m) => m.email).join(' '); - logger.logInfo('Preparing year, instrument and proposal', { - proposalId: proposalId, - year: year, - instrument: instrument, - group: group, - proposerEmail: proposerEmail, - membersEmails: membersEmails, - }); - - // update command - const command = (env.PROPOSAL_FOLDERS_CREATION_COMMAND! as string) - .replace(//, instrument) - .replace(//, year) - .replace(//, proposalId) - .replace(//, group) - .replace(//, proposerEmail) - .replace(//, membersEmails); - logger.logInfo('Command to be run', { command: command }); - - // run command - exec(command, (error, stdout, stderr) => { - if (error) { - logger.logError('Unable to create folders with error', { - command: command, - errorMessage: error.message, - }); + const instruments = proposalMessage.instruments.map((instrument) => + instrument.shortCode.toLowerCase() + ); + + for (const instrument of instruments) { + logger.logInfo('Preparing year, instrument and proposal', { + proposalId: proposalId, + year: year, + instrument: instrument, + group: group, + proposerEmail: proposerEmail, + membersEmails: membersEmails, + }); - return; - } - if (stderr) { - logger.logError('Unable to create folders with stderr', { + // update command + const command = (env.PROPOSAL_FOLDERS_CREATION_COMMAND! as string) + .replace(//, instrument) + .replace(//, year) + .replace(//, proposalId) + .replace(//, group) + .replace(//, proposerEmail) + .replace(//, membersEmails); + logger.logInfo('Command to be run', { command: command }); + + // run command + exec(command, (error, stdout, stderr) => { + if (error) { + logger.logError('Unable to create folders with error', { + command: command, + errorMessage: error.message, + }); + + return; + } + if (stderr) { + logger.logError('Unable to create folders with stderr', { + command: command, + stderr: stderr, + }); + + return; + } + logger.logInfo('Proposal folder creation successful', { command: command, - stderr: stderr, + output: stdout, }); - - return; - } - logger.logInfo('Proposal folder creation successful', { - command: command, - output: stdout, }); - }); + } }; export { proposalFoldersCreation }; diff --git a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts index b618c4d0..0e1d5ade 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumerCallbacks/upsertProposalInScicat.ts @@ -1,7 +1,11 @@ import { logger } from '@user-office-software/duo-logger'; +import { + Instrument, + InstrumentDto, +} from '../../../../../models/ProposalMessage'; import { ValidProposalMessageData } from '../../../utils/validateProposalMessage'; -import { CreateProposalDto } from '../dto'; +import { CreateProposalDto, UpdateProposalDto } from '../dto'; const sciCatBaseUrl = process.env.SCICAT_BASE_URL; const sciCatLoginEndpoint = process.env.SCICAT_LOGIN_ENDPOINT || '/Users/login'; @@ -10,22 +14,17 @@ const sciCatPassword = process.env.SCICAT_PASSWORD; async function request( url: string, - config: RequestInit, - fetchAsPlainText = false + config: RequestInit ): Promise { // NOTE: Node v18 comes with fetch API by default const response = await fetch(url, config); if (!response.ok) { - return response.text().then((text) => { - throw new Error(`An error occurred while sending the request: ${text}`); + return response.text().then((errorDetail) => { + throw new Error(errorDetail); }); } - if (fetchAsPlainText) { - return (await response.text()) as TResponse; - } - return (await response.json()) as TResponse; } @@ -65,15 +64,39 @@ const getCreateProposalDto = (proposalMessage: ValidProposalMessageData) => { lastname: proposalMessage.proposer.lastName, abstract: proposalMessage.abstract, ownerGroup: proposalMessage.shortCode, + instrumentIds: [], accessGroups: [], startTime: new Date(), endTime: new Date(), MeasurementPeriodList: [], + metadata: {}, }; return createProposalDto; }; +const getUpdateProposalDto = (proposalMessage: ValidProposalMessageData) => { + const updateProposalDto: UpdateProposalDto = { + title: proposalMessage.title, + pi_email: proposalMessage.proposer.email, + pi_firstname: proposalMessage.proposer.firstName, + pi_lastname: proposalMessage.proposer.lastName, + email: proposalMessage.proposer.email, + firstname: proposalMessage.proposer.firstName, + lastname: proposalMessage.proposer.lastName, + abstract: proposalMessage.abstract, + ownerGroup: proposalMessage.shortCode, + instrumentIds: [], + accessGroups: [], + startTime: new Date(), + endTime: new Date(), + MeasurementPeriodList: [], + metadata: {}, + }; + + return updateProposalDto; +}; + const createProposal = async ( proposalMessage: ValidProposalMessageData, sciCatAccessToken: string @@ -84,6 +107,13 @@ const createProposal = async ( logger.logInfo('POST', { url }); logger.logInfo('Proposal data', { proposalData: createProposalDto }); + // RabbitMQ message only provides shortCodes (instrument names). + // To persist proposals with proper references, we resolve those shortCodes to + // actual Instrument IDs from SciCat and store the instrumentIds in the record. + createProposalDto.instrumentIds = await getInstrumentIds( + proposalMessage.instruments + ); + const createProposalResponse = await request(url, { method: 'POST', body: JSON.stringify(createProposalDto), @@ -105,8 +135,14 @@ const updateProposal = async ( sciCatAccessToken: string ) => { const url = `${sciCatBaseUrl}/Proposals/${proposalMessage.shortCode}`; - const updateProposalDto = getCreateProposalDto(proposalMessage); + const updateProposalDto = getUpdateProposalDto(proposalMessage); + // RabbitMQ message only provides shortCodes (instrument names). + // To persist proposals with proper references, we resolve those shortCodes to + // actual Instrument IDs from SciCat and store the instrumentIds in the record. + updateProposalDto.instrumentIds = await getInstrumentIds( + proposalMessage.instruments + ); const updateProposalResponse = await request(url, { method: 'PATCH', body: JSON.stringify(updateProposalDto), @@ -129,26 +165,69 @@ const checkProposalExists = async ( proposalId: string, sciCatAccessToken: string ) => { - // NOTE: Get proposal by id in scicat-backend-next returns 200 always even if proposal does not exist. This is why we check if there is something in the body. const url = `${sciCatBaseUrl}/Proposals/${proposalId}`; - const fetchedProposalDataAsText = await request( - url, - { - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${sciCatAccessToken}`, - }, + const response = await request(url, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sciCatAccessToken}`, }, - true - ); + }).catch((error) => { + try { + const parsedError = JSON.parse(error.message); + if (parsedError.statusCode === 404) { + return false; + } + } catch (reason) { + logger.logError('Error parsing error message', { + error, + reason, + }); + } + throw error; + }); - if (fetchedProposalDataAsText) { + if (response) { return true; } else { return false; } }; +const getInstrumentIds = async (instruments: Instrument[]) => { + const sciCatAccessToken = await getSciCatAccessToken(); + const instrumentNames = instruments.map((inst) => inst.shortCode); + + const instrumentIds = []; + + for (const name of instrumentNames) { + const instrumentNameLowerCase = name.toLowerCase(); + + const filterString = JSON.stringify({ + where: { name: { ilike: instrumentNameLowerCase } }, + }); + + const url = `${sciCatBaseUrl}/Instruments?filter=${encodeURIComponent(filterString)}`; + + try { + const res = await request(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${sciCatAccessToken}`, + }, + }); + + instrumentIds.push(res[0].pid); + } catch (error) { + logger.logError(`Error fetching instrument ID from scicat for ${name}`, { + error, + }); + } + } + + return instrumentIds; +}; + const upsertProposalInScicat = async ( proposalMessage: ValidProposalMessageData ) => { diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts index 25d13f01..cc431a52 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer.ts @@ -1,7 +1,6 @@ import { ConsumerCallback } from '@user-office-software/duo-message-broker'; import { Event } from '../../../../../models/Event'; -import { ProposalMessageData } from '../../../../../models/ProposalMessage'; import { QueueConsumer } from '../../../QueueConsumer'; import { hasTriggeringStatus } from '../../../utils/hasTriggeringStatus'; import { hasTriggeringType } from '../../../utils/hasTriggeringType'; @@ -9,9 +8,8 @@ import { validateProposalMessage } from '../../../utils/validateProposalMessage' import { createChatroom } from '../consumerCallbacks/createChatroom'; const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, Event.PROPOSAL_STATUS_ACTION_EXECUTED, + Event.PROPOSAL_UPDATED, ]; const triggeringStatuses = process.env.SCICAT_PROPOSAL_TRIGGERING_STATUSES?.split(', '); @@ -38,9 +36,7 @@ export class ChatroomCreationQueueConsumer extends QueueConsumer { return; } - const proposalMessage = validateProposalMessage( - message as ProposalMessageData - ); + const proposalMessage = validateProposalMessage(message); createChatroom(proposalMessage); }; diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts index 1b4af1d9..fad6dc2e 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer.ts @@ -1,18 +1,13 @@ import { ConsumerCallback } from '@user-office-software/duo-message-broker'; import { Event } from '../../../../../models/Event'; -import { ProposalMessageData } from '../../../../../models/ProposalMessage'; import { QueueConsumer } from '../../../QueueConsumer'; import { hasTriggeringStatus } from '../../../utils/hasTriggeringStatus'; import { hasTriggeringType } from '../../../utils/hasTriggeringType'; -import { validateProposalMessage } from '../../../utils/validateMessages'; +import { validateProposalMessage } from '../../../utils/validateProposalMessage'; import { proposalFoldersCreation } from '../consumerCallbacks/proposalFoldersCreation'; -const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, - Event.PROPOSAL_STATUS_ACTION_EXECUTED, -]; +const EVENT_TYPES = [Event.PROPOSAL_STATUS_ACTION_EXECUTED]; const triggeringStatuses = process.env.PROPOSAL_FOLDERS_CREATION_TRIGGERING_STATUSES?.split(', '); @@ -39,9 +34,7 @@ export class FolderCreationQueueConsumer extends QueueConsumer { return; } - const proposalMessage = validateProposalMessage( - message as ProposalMessageData - ); + const proposalMessage = validateProposalMessage(message); proposalFoldersCreation(proposalMessage); }; diff --git a/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts b/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts index bf4c849a..e6f790c2 100644 --- a/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts +++ b/src/queue/consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer.ts @@ -1,7 +1,6 @@ import { ConsumerCallback } from '@user-office-software/duo-message-broker'; import { Event } from '../../../../../models/Event'; -import { ProposalMessageData } from '../../../../../models/ProposalMessage'; import { QueueConsumer } from '../../../QueueConsumer'; import { hasTriggeringStatus } from '../../../utils/hasTriggeringStatus'; import { hasTriggeringType } from '../../../utils/hasTriggeringType'; @@ -9,10 +8,10 @@ import { validateProposalMessage } from '../../../utils/validateProposalMessage' import { upsertProposalInScicat } from '../consumerCallbacks/upsertProposalInScicat'; const EVENT_TYPES = [ - Event.PROPOSAL_STATUS_CHANGED_BY_WORKFLOW, - Event.PROPOSAL_STATUS_CHANGED_BY_USER, Event.PROPOSAL_STATUS_ACTION_EXECUTED, + Event.PROPOSAL_UPDATED, ]; + const triggeringStatuses = process.env.SCICAT_PROPOSAL_TRIGGERING_STATUSES?.split(', '); @@ -38,9 +37,7 @@ export class ProposalCreationQueueConsumer extends QueueConsumer { return; } - const proposalMessage = validateProposalMessage( - message as ProposalMessageData - ); + const proposalMessage = validateProposalMessage(message); upsertProposalInScicat(proposalMessage); }; diff --git a/src/queue/consumers/scicat/scicatProposal/dto.ts b/src/queue/consumers/scicat/scicatProposal/dto.ts index 3ff3ff79..8aa79b07 100644 --- a/src/queue/consumers/scicat/scicatProposal/dto.ts +++ b/src/queue/consumers/scicat/scicatProposal/dto.ts @@ -12,9 +12,42 @@ export type CreateProposalDto = { abstract: string; startTime?: Date; endTime?: Date; + instrumentIds: string[]; MeasurementPeriodList: any[]; + metadata?: Record; }; +export type UpdateProposalDto = { + ownerGroup?: string; + accessGroups?: string[]; + pi_email?: string; + pi_firstname?: string; + pi_lastname?: string; + email: string; + firstname?: string; + lastname?: string; + title: string; + abstract?: string; + startTime?: Date; + endTime?: Date; + instrumentIds: string[]; + MeasurementPeriodList?: any[]; + metadata?: Record; +}; + +export interface Institution { + id: number; + name: string; + country?: number; + verified: boolean; + rorId?: string; +} + +export interface Country { + countryId?: number; + country?: string; +} + export interface ProposalUser { id: number; firstName: string; @@ -22,6 +55,8 @@ export interface ProposalUser { email: string; oidcSub: string; oauthIssuer?: string; + institution?: Institution; + country?: Country; } export interface ProposalAcceptedMessage { @@ -46,3 +81,35 @@ export type ChatRoom = { export type UserId = { user_id: string; }; + +export interface Threepid { + medium: string; + address: string; + added_at: number; + validated_at: number; +} + +export interface ExternalId { + auth_provider: string; + external_id: string; +} + +export interface SynapseUser { + name: string; // Fully-qualified user ID (e.g., @user:example.com) + displayname: string | null; // User's display name, can be null if not set + threepids: Threepid[]; // List of third-party IDs (e.g., emails) + avatar_url: string | null; // User's avatar URL, can be null if not set + is_guest: boolean; // Whether the user is a guest + admin: boolean; // Whether the user is a server administrator + deactivated: boolean; // Whether the user is deactivated + erased: boolean; // Whether the user is marked as erased (GDPR) + shadow_banned: boolean; // Whether the user is shadow banned + creation_ts: number; // User's creation timestamp (in ms since Unix epoch) + appservice_id: string | null; // ID of the appservice that registered the user, or null + consent_server_notice_sent: string | null; // Consent notice version, or null + consent_version: string | null; // Consent version, or null + consent_ts: number | null; // Consent timestamp, or null + external_ids: ExternalId[]; // List of external IDs associated with the user + user_type: string | null; // Type of user (e.g., bot, support), or null + locked: boolean; +} diff --git a/src/queue/consumers/utils/collectUsersFromProposalMessage.spec.ts b/src/queue/consumers/utils/collectUsersFromProposalMessage.spec.ts new file mode 100644 index 00000000..0de84c4d --- /dev/null +++ b/src/queue/consumers/utils/collectUsersFromProposalMessage.spec.ts @@ -0,0 +1,91 @@ +import { collectUsersFromProposalMessage } from './collectUsersFromProposalMessage'; +import { ProposalMessageData } from '../../../models/ProposalMessage'; +import { ProposalUser } from '../scicat/scicatProposal/dto'; + +describe('collectUsersFromProposalMessage', () => { + const createUser = (id: number, suffix: string): ProposalUser => ({ + id, + firstName: `first-${suffix}`, + lastName: `last-${suffix}`, + email: `${suffix}@example.com`, + oidcSub: `oidc-${suffix}`, + }); + + const createBaseMessage = ( + overrides: Partial = {} + ): ProposalMessageData => ({ + proposalPk: 1, + shortCode: 'short', + title: 'title', + abstract: 'abstract', + callId: 2, + submitted: true, + members: [], + ...overrides, + }); + + it('returns members, proposer, data access users, and visitors in order', () => { + const member = createUser(1, 'member'); + const proposer = createUser(2, 'proposer'); + const dataAccessUser = createUser(3, 'data'); + const visitor = createUser(4, 'visitor'); + + const message = createBaseMessage({ + members: [member], + proposer, + dataAccessUsers: [dataAccessUser], + visitors: [visitor], + }); + + const result = collectUsersFromProposalMessage(message); + + expect(result).toEqual([member, proposer, dataAccessUser, visitor]); + }); + + it('filters out undefined entries', () => { + const member = createUser(1, 'member'); + + const message = createBaseMessage({ + members: [member], + proposer: undefined, + dataAccessUsers: [undefined as unknown as ProposalUser], + }); + + const result = collectUsersFromProposalMessage(message); + + expect(result).toEqual([member]); + }); + + it('handles missing dataAccessUsers by treating it as empty', () => { + const member = createUser(1, 'member'); + const proposer = createUser(2, 'proposer'); + + const message = { + ...createBaseMessage({ + members: [member], + proposer, + }), + } as ProposalMessageData; + + const result = collectUsersFromProposalMessage(message); + + expect(result).toEqual([member, proposer]); + }); + + it('handles missing visitors by treating it as empty', () => { + const member = createUser(1, 'member'); + const proposer = createUser(2, 'proposer'); + + const message = { + ...createBaseMessage({ + members: [member], + proposer, + dataAccessUsers: [], + }), + } as ProposalMessageData; + + const result = collectUsersFromProposalMessage(message); + + expect(result).toEqual([member, proposer]); + }); +}); diff --git a/src/queue/consumers/utils/collectUsersFromProposalMessage.ts b/src/queue/consumers/utils/collectUsersFromProposalMessage.ts new file mode 100644 index 00000000..18bf16ed --- /dev/null +++ b/src/queue/consumers/utils/collectUsersFromProposalMessage.ts @@ -0,0 +1,13 @@ +import { ProposalMessageData } from '../../../models/ProposalMessage'; +import { ProposalUser } from '../scicat/scicatProposal/dto'; + +export function collectUsersFromProposalMessage({ + members, + proposer, + dataAccessUsers = [], + visitors = [], +}: ProposalMessageData): ProposalUser[] { + return [...members, proposer, ...dataAccessUsers, ...visitors].filter( + (user): user is ProposalUser => user !== undefined + ); +} diff --git a/src/queue/consumers/utils/validateMessages.ts b/src/queue/consumers/utils/validateMessages.ts deleted file mode 100644 index 84f2415b..00000000 --- a/src/queue/consumers/utils/validateMessages.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { MoodleMessageData } from '../../../models/MoodleMessage'; -import { ProposalMessageData } from '../../../models/ProposalMessage'; -export type ValidProposalMessageData = Required; - -export function validateProposalMessage( - proposalMessage: ProposalMessageData -): ValidProposalMessageData { - if (!proposalMessage.title) { - throw new Error('Proposal title is missing'); - } - - if (!proposalMessage.proposer) { - throw new Error('Proposal proposer is missing'); - } - - if (!proposalMessage.proposer.firstName) { - throw new Error('Proposal proposer first name is missing'); - } - - if (!proposalMessage.proposer.lastName) { - throw new Error('Proposal proposer last name is missing'); - } - - if (!proposalMessage.proposer.email) { - throw new Error('Proposal proposer email is missing'); - } - - if (!proposalMessage.abstract) { - throw new Error('Proposal abstract is missing'); - } - - if (!proposalMessage.shortCode) { - throw new Error('Proposal short code is missing'); - } - - if (!proposalMessage.instrument) { - throw new Error('Instrument is missing'); - } - - return proposalMessage as ValidProposalMessageData; -} - -export function validateMoodleMessage( - moodleMessage: MoodleMessageData -): ValidMoodleMessageData { - if (!moodleMessage.enrolled_user_id) { - throw new Error('Property userid is missing'); - } - - if (!moodleMessage.course_short_name) { - throw new Error('Property courseid is missing'); - } - - return { - enrolled_user_id: moodleMessage.enrolled_user_id, - course_short_name: moodleMessage.course_short_name, - }; -} - -// NOTE: -// context can be: instrument_shortCode, course_id -// item can be: (proposal_shortCode, user_id) -export type ValidMoodleMessageData = { - enrolled_user_id: string; - course_short_name: string; -}; diff --git a/src/queue/consumers/utils/validateMessages.spec.ts b/src/queue/consumers/utils/validateProposalMessage.spec.ts similarity index 79% rename from src/queue/consumers/utils/validateMessages.spec.ts rename to src/queue/consumers/utils/validateProposalMessage.spec.ts index d9608e96..1f608865 100644 --- a/src/queue/consumers/utils/validateMessages.spec.ts +++ b/src/queue/consumers/utils/validateProposalMessage.spec.ts @@ -1,4 +1,4 @@ -import { validateProposalMessage } from './validateMessages'; +import { validateProposalMessage } from './validateProposalMessage'; describe('Validate messages', () => { it('should throw error when message is not valid', () => { @@ -15,8 +15,10 @@ describe('Validate messages', () => { members: [], proposalPk: 1, shortCode: '123123', - instrument: { id: 1, shortCode: 'TEST' }, + instruments: [{ id: 1, shortCode: 'TEST', allocatedTime: 1 }], newStatus: 'REVIEW', + callId: 123, + submitted: true, proposer: { id: 1, firstName: 'Test', diff --git a/src/queue/consumers/utils/validateProposalMessage.ts b/src/queue/consumers/utils/validateProposalMessage.ts index f3c4d58f..5dee51b7 100644 --- a/src/queue/consumers/utils/validateProposalMessage.ts +++ b/src/queue/consumers/utils/validateProposalMessage.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { ProposalMessageData } from '../../../models/ProposalMessage'; export type ValidProposalMessageData = Required; export function validateProposalMessage( - proposalMessage: ProposalMessageData + proposalMessage: any ): ValidProposalMessageData { if (!proposalMessage.title) { throw new Error('Proposal title is missing'); @@ -28,13 +29,35 @@ export function validateProposalMessage( throw new Error('Proposal abstract is missing'); } + if (!proposalMessage.callId) { + throw new Error('Proposal CallId is missing'); + } + + if (!proposalMessage.submitted) { + throw new Error('Proposal Submitted status is missing'); + } + if (!proposalMessage.shortCode) { throw new Error('Proposal short code is missing'); } - if (!proposalMessage.instrument) { - throw new Error('Instrument is missing'); + if (!proposalMessage.instruments?.length) { + throw new Error('Instruments are missing'); } + proposalMessage.instruments.forEach((instrument: any) => { + if (!instrument.id) { + throw new Error('Instrument id is missing'); + } + + if (!instrument.shortCode) { + throw new Error('Instrument short code is missing'); + } + + if (typeof instrument.allocatedTime !== 'number') { + throw new Error('Instrument allocated time is missing'); + } + }); + return proposalMessage as ValidProposalMessageData; } diff --git a/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts b/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts new file mode 100644 index 00000000..cbb79e22 --- /dev/null +++ b/src/queue/consumers/visa/consumerCallbacks/syncVisaProposal.ts @@ -0,0 +1,127 @@ +import { container } from 'tsyringe'; + +import { Tokens } from '../../../../config/Tokens'; +import { ExperimentDataSource } from '../../../../datasources/visa/ExperimentDataSource'; +import { ExperimentUserDataSource } from '../../../../datasources/visa/ExperimentUserDataSource'; +import { InstrumentDataSource } from '../../../../datasources/visa/InstrumentDataSource'; +import { ProposalDataSource } from '../../../../datasources/visa/ProposalDataSource'; +import { UserDataSource } from '../../../../datasources/visa/UserDataSource'; +import { ProposalUser } from '../../scicat/scicatProposal/dto'; +import { ValidProposalMessageData } from '../../utils/validateProposalMessage'; + +async function createUserAndAssignToExperiment( + user: ProposalUser, + proposalPk: number +) { + const userDataSource = container.resolve( + Tokens.VisaUserDataSource + ); + + const experimentDataSource = container.resolve( + Tokens.VisaExperimentDataSource + ); + + const experimentUserDataSource = container.resolve( + Tokens.VisaExperimentUserDataSource + ); + + const createdUser = await userDataSource.create(user); + const experiment = await experimentDataSource.getByProposalId(proposalPk); + if (experiment && createdUser) { + await experimentUserDataSource.create({ + experimentId: experiment.id, + userId: createdUser.id, + }); + } +} + +async function deleteMissingUsersFromExperiment( + proposalPk: number, + allProposalUsers: ProposalUser[] +) { + const experimentDataSource = container.resolve( + Tokens.VisaExperimentDataSource + ); + const experimentUserDataSource = container.resolve( + Tokens.VisaExperimentUserDataSource + ); + + const experiment = await experimentDataSource.getByProposalId(proposalPk); + if (!experiment) return; + + const experimentUsers = + await experimentUserDataSource.getAllUsersByExperimentId( + experiment.id.toString() + ); + + for (const experimentUser of experimentUsers) { + if ( + !allProposalUsers.some((member) => member.oidcSub === experimentUser.id) + ) { + await experimentUserDataSource.delete({ + experimentId: experiment.id, + userId: experimentUser.id, + }); + } + } +} +export async function syncVisaProposal( + proposalWithNewStatus: ValidProposalMessageData +) { + const proposalDatasource = container.resolve( + Tokens.VisaProposalDataSource + ); + + const experimentDataSource = container.resolve( + Tokens.VisaExperimentDataSource + ); + + const instrumentDataSource = container.resolve( + Tokens.VisaInstrumentDataSource + ); + + // Create New Proposal + let proposal = await proposalDatasource.get(proposalWithNewStatus.proposalPk); + if (!proposal) { + proposal = await proposalDatasource.create(proposalWithNewStatus); + } + // Get Instrument + for (const proposalInstrument of proposalWithNewStatus.instruments) { + let instrument = await instrumentDataSource.getByShortCode( + proposalInstrument.shortCode + ); + + if (!instrument) { + instrument = await instrumentDataSource.create({ + id: proposalInstrument.id, + name: proposalInstrument.shortCode, + }); + } + // Assign Instrument to Proposal and create an Experiment + await experimentDataSource.create({ + proposalPk: proposal.id, + instrumentId: instrument.id, + }); + } + + const experimenters = [ + ...(proposalWithNewStatus.proposer ? [proposalWithNewStatus.proposer] : []), + ...proposalWithNewStatus.members, + ...(proposalWithNewStatus.dataAccessUsers || []), + ...(proposalWithNewStatus.visitors || []), + ]; + + // Create new user for the Principal Investigator, Coproposers, Data Access Users and Visitors + for (const member of experimenters) { + await createUserAndAssignToExperiment( + member, + proposalWithNewStatus.proposalPk + ); + } + + // Delete the users that are saved in the experiment users table, but not in the Proposal Payload + await deleteMissingUsersFromExperiment( + proposalWithNewStatus.proposalPk, + experimenters + ); +} diff --git a/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts b/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts new file mode 100644 index 00000000..1347bb4f --- /dev/null +++ b/src/queue/consumers/visa/consumers/syncProposalQueueConsumer.ts @@ -0,0 +1,50 @@ +import { logger } from '@user-office-software/duo-logger'; +import { ConsumerCallback } from '@user-office-software/duo-message-broker'; + +import { Event } from '../../../../models/Event'; +import { ProposalMessageData } from '../../../../models/ProposalMessage'; +import { QueueConsumer } from '../../QueueConsumer'; +import { hasTriggeringStatus } from '../../utils/hasTriggeringStatus'; +import { hasTriggeringType } from '../../utils/hasTriggeringType'; +import { validateProposalMessage } from '../../utils/validateProposalMessage'; +import { syncVisaProposal } from '../consumerCallbacks/syncVisaProposal'; +import { sanitizeProposalMessage } from '../utils/sanitizeProposalMessage'; + +const EVENTS_FOR_HANDLING = [ + Event.PROPOSAL_STATUS_ACTION_EXECUTED, + Event.PROPOSAL_UPDATED, +]; + +const triggeringStatuses = + process.env.VISA_SYNCING_TRIGGERING_STATUSES?.split(', '); + +export class SyncProposalQueueConsumer extends QueueConsumer { + getQueueName(): string { + return process.env.VISA_QUEUE_NAME as string; + } + getExchangeName(): string { + return process.env.USER_OFFICE_CORE_EXCHANGE_NAME as string; + } + onMessage: ConsumerCallback = async (type, message) => { + if (!hasTriggeringType(type, EVENTS_FOR_HANDLING)) { + return; + } + + logger.logInfo('VisaQueueConsumer', { + type, + message, + }); + + const hasStatus = hasTriggeringStatus(message, triggeringStatuses); + + if (!hasStatus) { + return; + } + + const proposalMessage = validateProposalMessage( + sanitizeProposalMessage(message as ProposalMessageData) + ); + + await syncVisaProposal(proposalMessage); + }; +} diff --git a/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts b/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts new file mode 100644 index 00000000..393ddf3c --- /dev/null +++ b/src/queue/consumers/visa/utils/sanitizeProposalMessage.ts @@ -0,0 +1,35 @@ +import { ProposalMessageData } from '../../../../models/ProposalMessage'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export function sanitizeProposalMessage(proposalMessage: ProposalMessageData) { + return { + ...proposalMessage, + members: proposalMessage.members.map((member) => ({ + ...member, + oidcSub: member.oidcSub.toLowerCase(), + email: member.email.toLowerCase(), + })), + dataAccessUsers: proposalMessage.dataAccessUsers + ? proposalMessage.dataAccessUsers.map((user) => ({ + ...user, + oidcSub: user.oidcSub.toLowerCase(), + email: user.email.toLowerCase(), + })) + : [], + visitors: proposalMessage.visitors + ? proposalMessage.visitors.map((visitor) => ({ + ...visitor, + oidcSub: visitor.oidcSub.toLowerCase(), + email: visitor.email.toLowerCase(), + })) + : [], + proposer: proposalMessage.proposer + ? { + ...proposalMessage.proposer, + oidcSub: proposalMessage.proposer.oidcSub.toLowerCase(), + email: proposalMessage.proposer.email.toLowerCase(), + } + : undefined, + }; +} diff --git a/src/queue/messageBroker/getMockMessageBroker.ts b/src/queue/messageBroker/getMockMessageBroker.ts index 036d2942..3a0603c1 100644 --- a/src/queue/messageBroker/getMockMessageBroker.ts +++ b/src/queue/messageBroker/getMockMessageBroker.ts @@ -37,7 +37,7 @@ class MockMessageBroker implements MessageBroker { sendBroadcast(queue: Queue, type: string, message: string): Promise { throw new Error('Method not implemented.'); } - listenOn(queue: Queue, cb: ConsumerCallback): void { + listenOn(queue: Queue, cb: ConsumerCallback): Promise { throw new Error('Method not implemented.'); } listenOnBroadcast(cb: ConsumerCallback): void { diff --git a/src/queue/queueHandling.ts b/src/queue/queueHandling.ts index f672239e..e221b72e 100644 --- a/src/queue/queueHandling.ts +++ b/src/queue/queueHandling.ts @@ -1,44 +1,35 @@ import { container } from 'tsyringe'; import { MoodleFolderCreationQueueConsumer } from './consumers/moodle/MoodleFolderCreationQueueConsumer'; +import { OneIdentityIntegrationQueueConsumer } from './consumers/oneidentity/OneIdentityIntegrationQueueConsumer'; import { ChatroomCreationQueueConsumer } from './consumers/scicat/scicatProposal/consumers/ChatroomCreationQueueConsumer'; import { FolderCreationQueueConsumer } from './consumers/scicat/scicatProposal/consumers/FolderCreationQueueConsumer'; import { ProposalCreationQueueConsumer } from './consumers/scicat/scicatProposal/consumers/ProposalCreationQueueConsumer'; +import { SyncProposalQueueConsumer } from './consumers/visa/consumers/syncProposalQueueConsumer'; import { GetMessageBroker } from './messageBroker/getMessageBroker'; import { Tokens } from '../config/Tokens'; import { str2Bool } from '../config/utils'; -const ENABLE_SCICAT_PROPOSAL_UPSERT = str2Bool( - process.env.ENABLE_SCICAT_PROPOSAL_UPSERT as string -); -const ENABLE_SCICHAT_ROOM_CREATION = str2Bool( - process.env.ENABLE_SCICHAT_ROOM_CREATION as string -); -const ENABLE_PROPOSAL_FOLDERS_CREATION = str2Bool( - process.env.ENABLE_PROPOSAL_FOLDERS_CREATION as string -); -const ENABLE_MOODLE_FOLDERS_CREATION = str2Bool( - process.env.ENABLE_MOODLE_FOLDERS_CREATION as string -); - const getMessageBroker: GetMessageBroker = container.resolve( Tokens.ProvideMessageBroker ); +const queueConsumers = { + ENABLE_SCICAT_PROPOSAL_UPSERT: ProposalCreationQueueConsumer, + ENABLE_SCICHAT_ROOM_CREATION: ChatroomCreationQueueConsumer, + ENABLE_PROPOSAL_FOLDERS_CREATION: FolderCreationQueueConsumer, + ENABLE_MOODLE_FOLDERS_CREATION: MoodleFolderCreationQueueConsumer, + ENABLE_ONE_IDENTITY_INTEGRATION: OneIdentityIntegrationQueueConsumer, + ENABLE_SYNC_VISA_PROPOSALS: SyncProposalQueueConsumer, +}; + const startQueueHandling = async (): Promise => { const messageBroker = await getMessageBroker(); - if (ENABLE_SCICAT_PROPOSAL_UPSERT) { - new ProposalCreationQueueConsumer(messageBroker); - } - if (ENABLE_SCICHAT_ROOM_CREATION) { - new ChatroomCreationQueueConsumer(messageBroker); - } - if (ENABLE_PROPOSAL_FOLDERS_CREATION) { - new FolderCreationQueueConsumer(messageBroker); - } - if (ENABLE_MOODLE_FOLDERS_CREATION) { - new MoodleFolderCreationQueueConsumer(messageBroker); + for (const [envVar, QueueConsumer] of Object.entries(queueConsumers)) { + if (str2Bool(process.env[envVar] as string)) { + new QueueConsumer(messageBroker); + } } }; diff --git a/src/services/synapse/SynapseService.spec.ts b/src/services/synapse/SynapseService.spec.ts new file mode 100644 index 00000000..a4701d58 --- /dev/null +++ b/src/services/synapse/SynapseService.spec.ts @@ -0,0 +1,345 @@ +jest.mock('@user-office-software/duo-logger'); +jest.mock('matrix-js-sdk', () => ({ + ...jest.requireActual('matrix-js-sdk'), + createClient: jest.fn(), +})); +jest.mock('./produceSynapseUserId', () => ({ + produceSynapseUserId: jest.fn(), +})); + +import { logger } from '@user-office-software/duo-logger'; +import { AxiosError } from 'axios'; +import { createClient } from 'matrix-js-sdk'; + +import { produceSynapseUserId } from './produceSynapseUserId'; +import { SynapseService } from './SynapseService'; + +describe('SynapseService', () => { + let synapseService: SynapseService; + let mockLoggerLogError: jest.SpyInstance; + let mockLoggerLogInfo: jest.SpyInstance; + + const serviceAccountSynapseId = `@${process.env.SYNAPSE_SERVICE_USER}:${process.env.SYNAPSE_SERVER_NAME}`; + const roomId = '!randomRoom:ess'; + const member = { + id: 1111, + email: 'john.doe@example.com', + firstName: 'John', + lastName: 'Doe', + oidcSub: 'john.doe', + oauthIssuer: 'oidc-ping', + }; + const joinedRoomMembers = { + joined: { + ['user1']: { display_name: 'User1' }, + ['john.doe']: { display_name: 'John Doe' }, + [serviceAccountSynapseId]: { display_name: 'ServiceAccount' }, + }, + }; + const synapseUser = { + name: '@user:example.com', + displayname: 'User', + threepids: [], + avatar_url: null, + is_guest: 0, + admin: 0, + deactivated: true, + erased: false, + shadow_banned: 0, + creation_ts: 1560432506, + appservice_id: null, + consent_server_notice_sent: null, + consent_version: null, + consent_ts: null, + external_ids: [], + user_type: null, + locked: false, + }; + const mockCreateClient = { + loginWithPassword: jest.fn(), + http: { + authedRequest: jest.fn(), + }, + }; + + beforeEach(() => { + jest.resetAllMocks(); + (createClient as jest.Mock).mockReturnValue(mockCreateClient); + mockLoggerLogError = jest.spyOn(logger, 'logError'); + mockLoggerLogInfo = jest.spyOn(logger, 'logInfo'); + process.env.SYNAPSE_SERVICE_USER = 'serviceUser'; + process.env.SYNAPSE_SERVER_NAME = 'matrix.org'; + synapseService = new SynapseService(); + }); + + describe('invite', () => { + const validReason = `@${member.oidcSub}:ess already in the room`; + const InvalidReason = 'invalid reason'; + const memberToBeRemoved = { + id: 1112, + email: 'user1@example.com', + firstName: 'user1', + lastName: 'test', + oidcSub: 'user1', + oauthIssuer: 'oidc-ping', + }; + + it("should not log any errors when the reason message includes 'already in the room", async () => { + const joinedRoomMembersSet = new Set(''); + jest + .spyOn(synapseService, 'getRoomMembers') + .mockResolvedValueOnce(joinedRoomMembersSet); + + (produceSynapseUserId as jest.Mock).mockResolvedValueOnce(member.oidcSub); + + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(validReason) + ); + + const result = await synapseService.invite(roomId, [member]); + + expect(mockLoggerLogError).not.toHaveBeenCalledWith( + 'Failed to invite user', + { + message: validReason, + member: member, + } + ); + expect(result[0]).toEqual({ invited: false, userId: member.oidcSub }); + }); + it("should log errors when the reason message does not include 'already in the room'", async () => { + const joinedRoomMembersSet = new Set(''); + jest + .spyOn(synapseService, 'getRoomMembers') + .mockResolvedValueOnce(joinedRoomMembersSet); + + (produceSynapseUserId as jest.Mock).mockResolvedValueOnce(member.oidcSub); + + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(InvalidReason) + ); + + const result = await synapseService.invite(roomId, [member]); + + expect(mockLoggerLogError).toHaveBeenCalledWith('Failed to invite user', { + message: InvalidReason, + member: member, + }); + expect(result[0]).toEqual({ invited: false, userId: member.oidcSub }); + }); + + it('should remove the user from the chatroom if the updated proposal does not include the previous member in the room', async () => { + const joinedRoomMembersSet = new Set( + Object.keys(joinedRoomMembers.joined) + ); + + jest + .spyOn(synapseService, 'getRoomMembers') + .mockResolvedValueOnce(joinedRoomMembersSet); + + const removeUserFromRoomSpy = jest + .spyOn(synapseService, 'removeUserFromRoom') + .mockResolvedValueOnce(); + + (produceSynapseUserId as jest.Mock) + .mockResolvedValueOnce(member.oidcSub) + .mockResolvedValueOnce(memberToBeRemoved.oidcSub); + + mockCreateClient.http.authedRequest + .mockResolvedValueOnce('/rooms/${roomId}/joined_members Call') + .mockResolvedValueOnce('/join/${roomId} Call') + .mockResolvedValueOnce('/rooms/${roomId}/kick Call'); + + const result = await synapseService.invite(roomId, [member]); + + expect(removeUserFromRoomSpy).toHaveBeenCalledWith( + roomId, + memberToBeRemoved.oidcSub + ); + expect(result).toEqual([{ userId: member.oidcSub, invited: true }]); + expect(result).not.toContainEqual({ + userId: memberToBeRemoved.oidcSub, + invited: true, + }); + }); + }); + + describe('getUserByOidcSub', () => { + const validReason = 'User not found'; + const InvalidReason = 'invalid reason'; + + it("should not log any errors when the reason message includes 'User not found'", async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(validReason) + ); + + const result = await synapseService.getUserByOidcSub(member.oidcSub); + + expect(mockLoggerLogError).not.toHaveBeenCalledWith( + 'Not able to find user by oidc_sub', + { + message: validReason, + } + ); + + expect(result).toEqual(undefined); + }); + + it("should log errors when the reason message does not include 'User not found'", async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(InvalidReason) + ); + + const result = await synapseService.getUserByOidcSub(member.oidcSub); + + expect(mockLoggerLogError).toHaveBeenCalledWith( + 'Not able to find user by oidc_sub', + { + message: InvalidReason, + } + ); + + expect(result).toEqual(undefined); + }); + }); + + describe('getUserByEmail', () => { + const validReason = 'User not found'; + const InvalidReason = 'invalid reason'; + + it("should not log any errors when the reason message includes 'User not found'", async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(validReason) + ); + + const result = await synapseService.getUserByEmail(member.email); + + expect(mockLoggerLogError).not.toHaveBeenCalledWith( + 'Not able to find user by Email', + { + message: validReason, + } + ); + + expect(result).toEqual(undefined); + }); + + it("should log errors when the reason message does not include 'User not found'", async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(InvalidReason) + ); + + const result = await synapseService.getUserByEmail(member.email); + + expect(mockLoggerLogError).toHaveBeenCalledWith( + 'Not able to find user by Email', + { + message: InvalidReason, + } + ); + + expect(result).toEqual(undefined); + }); + }); + + describe('getUserInfo', () => { + const unknownError = 'unknown Error'; + + it('should get detailed user information', async () => { + mockCreateClient.http.authedRequest.mockResolvedValueOnce(synapseUser); + + const result = await synapseService.getUserInfo(synapseUser.name); + + expect(result).toEqual(synapseUser); + }); + + it('should log errors if the user information is not reterivable', async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(unknownError) + ); + + const result = await synapseService.getUserInfo(synapseUser.name); + + expect(mockLoggerLogError).toHaveBeenCalledWith( + 'Not able to get user information', + { + message: unknownError, + } + ); + + expect(result).toEqual(undefined); + }); + }); + + describe('getRoomMembers', () => { + const unknownError = 'unknown Error'; + + it('should get all the members from the room except the service account', async () => { + mockCreateClient.http.authedRequest.mockResolvedValueOnce( + joinedRoomMembers + ); + const result = await synapseService.getRoomMembers(roomId); + + const noServiceAccount = new Set(Object.keys(joinedRoomMembers.joined)); + noServiceAccount.delete(serviceAccountSynapseId); + + expect(result).toEqual(noServiceAccount); + }); + + it('should log errors if the room members are not retrievable', async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(unknownError) + ); + + await expect(synapseService.getRoomMembers(roomId)).rejects.toThrow( + unknownError + ); + + expect(mockLoggerLogError).toHaveBeenCalledWith( + 'Failed to get joined room members', + { + reason: new AxiosError(unknownError), + roomId, + } + ); + }); + }); + + describe('removeUserFromRoom', () => { + const unknownError = 'unknown Error'; + const userId = 'user1'; + + it('should remove a user from the room', async () => { + mockCreateClient.http.authedRequest.mockResolvedValueOnce({ + roomId, + userId, + }); + + await synapseService.removeUserFromRoom(roomId, userId); + + expect(mockLoggerLogInfo).toHaveBeenCalledWith('Removed user from room', { + roomId, + userId, + }); + }); + + it('should log errors if the user cannot be removed from the room', async () => { + mockCreateClient.http.authedRequest.mockRejectedValueOnce( + new AxiosError(unknownError) + ); + + await expect( + synapseService.removeUserFromRoom(roomId, userId) + ).rejects.toThrow(unknownError); + + expect(mockLoggerLogError).toHaveBeenCalledWith( + 'Failed to remove user from room', + { + message: unknownError, + roomId, + userId, + } + ); + }); + }); +}); diff --git a/src/services/synapse/SynapseService.ts b/src/services/synapse/SynapseService.ts index 2fb2c399..9bee5e8e 100644 --- a/src/services/synapse/SynapseService.ts +++ b/src/services/synapse/SynapseService.ts @@ -7,6 +7,7 @@ import { createClient, EventType, MsgType, + RoomMember, } from 'matrix-js-sdk'; import { produceSynapseUserId } from './produceSynapseUserId'; @@ -15,6 +16,7 @@ import { ProposalUser, ChatRoom, UserId, + SynapseUser, } from '../../queue/consumers/scicat/scicatProposal/dto'; interface MemberObject { @@ -52,13 +54,36 @@ export class SynapseService { baseUrl: serverUrl, fetchFn: axiosFetch, }); - // TODO, If consumer service is started after downtime, and there are some pending messages in the queue // then it could be that queue handler will delegate handling of messages before connection to supabase is established - this.client.loginWithPassword( - serviceAccount.userId, - serviceAccount.password - ); + } + + async login(consumerName = 'ChatroomCreationQueueConsumer') { + if (!serviceAccount.userId) + throw new Error('SYNAPSE_SERVICE_USER is not set'); + if (!serviceAccount.password) + throw new Error('SYNAPSE_SERVICE_PASSWORD is not set'); + + try { + await this.client.loginWithPassword( + serviceAccount.userId, + serviceAccount.password + ); + } catch (error) { + logger.logError(`Failed to login to Synapse from ${consumerName}`, { + error, + }); + throw error; + } + } + + async logout() { + try { + await this.client.logout(); + } catch (error) { + logger.logError('Failed to logout from Synapse', { error }); + throw error; + } } async createRoom(name: string, topic: string, members: ProposalUser[]) { @@ -125,12 +150,6 @@ export class SynapseService { await this.client .sendEvent(roomId, EventType.RoomMessage, messageContent, '') - .then(() => { - logger.logInfo('Success sending message to chatroom ', { - roomId: roomId, - message: message, - }); - }) .catch((reason) => { logger.logError('Failed sending message to chatroom', { roomId: roomId, @@ -143,6 +162,8 @@ export class SynapseService { async invite(roomId: string, members: ProposalUser[]) { const invitedUsers: { userId: string; invited: boolean }[] = []; + const usersToBeRemoved = await this.getRoomMembers(roomId); + for (const member of members) { const userId = await produceSynapseUserId(member, this); await this.client.http @@ -158,10 +179,22 @@ export class SynapseService { invitedUsers.push({ userId, invited: true }); }) .catch((reason) => { - logger.logError('Failed to invite user', { reason, member }); + if (!reason.message.includes('already in the room')) { + logger.logError('Failed to invite user', { + message: reason?.message, + member, + }); + } invitedUsers.push({ userId, invited: false }); // don't throw, we want to invite all members }); + usersToBeRemoved.delete(userId); + } + + if (usersToBeRemoved.size > 0) { + for (const userId of usersToBeRemoved) { + await this.removeUserFromRoom(roomId, userId); + } } return invitedUsers; @@ -181,11 +214,11 @@ export class SynapseService { return response.rooms; } - async getUserByOidcSub(member: ProposalUser) { + async getUserByOidcSub(oidcSub: string) { const result = await this.client.http - .authedRequest( + .authedRequest( Method.Get, - `/auth_providers/${oauthIssuer}/users/${member.oidcSub}`, + `/auth_providers/${oauthIssuer}/users/${oidcSub}`, {}, undefined, { @@ -193,17 +226,24 @@ export class SynapseService { } ) .catch((reason) => { - logger.logError('Not able to find user by oidc_sub', { reason }); + if (!reason.message.includes('User not found')) { + logger.logError('Not able to find user by oidc_sub', { + message: reason.message, + }); + } + + return undefined; }); - return result as UserId; + return result; } async getUserByEmail(email: string) { + const lowerCaseEmail = email.toLowerCase(); const result = await this.client.http - .authedRequest( + .authedRequest( Method.Get, - `/threepid/${thirdPartyId}/users/${email}`, + `/threepid/${thirdPartyId}/users/${lowerCaseEmail}`, {}, undefined, { @@ -211,10 +251,92 @@ export class SynapseService { } ) .catch((reason) => { - logger.logError('Not able to find user by Email', { reason }); + if (!reason.message.includes('User not found')) { + logger.logError('Not able to find user by Email', { + message: reason.message, + }); + } + + return undefined; }); - return result as UserId; + return result; + } + + async getRoomMembers(roomId: string): Promise> { + // Get all joined room members except service account + const serviceAccountSynapseId = `@${serviceAccount.userId}:${serverName}`; + + const joinedRoomMembers = await this.client.http + .authedRequest<{ joined: Record }>( + Method.Get, + `/rooms/${roomId}/joined_members`, + {}, + undefined, + { prefix: CLIENT_API_PREFIX_V1 } + ) + .then((response) => { + return new Set( + Object.keys(response.joined).filter( + (userId) => userId !== serviceAccountSynapseId + ) + ); + }) + .catch((reason) => { + logger.logError('Failed to get joined room members', { + reason, + roomId, + }); + throw reason; + }); + + return joinedRoomMembers; + } + + async removeUserFromRoom(roomId: string, userId: string) { + return this.client.http + .authedRequest( + Method.Post, + `/rooms/${roomId}/kick`, + {}, + { user_id: userId }, + { + prefix: CLIENT_API_PREFIX_V1, + } + ) + .then(() => { + logger.logInfo('Removed user from room', { roomId, userId }); + }) + .catch((reason) => { + logger.logError('Failed to remove user from room', { + message: reason.message, + roomId, + userId, + }); + throw reason; + }); + } + + async getUserInfo(userId: string) { + const result = await this.client.http + .authedRequest( + Method.Get, + `/users/${userId}`, + {}, + undefined, + { + prefix: ADMIN_API_PREFIX_V2, + } + ) + .catch((reason) => { + logger.logError('Not able to get user information', { + message: reason.message, + }); + + return undefined; + }); + + return result; } async getRoomIdByName(name: string) { @@ -270,16 +392,16 @@ export class SynapseService { return result as User; } - async userExists(member: ProposalUser) { - const userExists = - !!(await this.getUserByOidcSub(member)) || - !!(await this.getUserByEmail(member.email)); + async getUserId(member: ProposalUser) { + const user = + (await this.getUserByOidcSub(member.oidcSub)) || + (await this.getUserByEmail(member.email)); - if (!userExists) { + if (!user) { logger.logInfo('User not exists: ', { member }); } - return userExists; + return user; } async createUser(member: ProposalUser, password: string) { @@ -308,7 +430,10 @@ export class SynapseService { { prefix: ADMIN_API_PREFIX_V2 } ) .catch((reason) => { - logger.logError('Failed to create user', { reason, member }); + logger.logError('Failed to create user', { + message: reason.message, + member, + }); throw reason; }); diff --git a/src/services/synapse/produceSynapseUserId.spec.ts b/src/services/synapse/produceSynapseUserId.spec.ts index 982b4b5a..f24d2045 100644 --- a/src/services/synapse/produceSynapseUserId.spec.ts +++ b/src/services/synapse/produceSynapseUserId.spec.ts @@ -1,13 +1,34 @@ +const serverName = (process.env.SYNAPSE_SERVER_NAME = 'test-server'); + import { produceSynapseUserId } from './produceSynapseUserId'; -test('Should produce valid user id', async () => { - const member = { - id: 1, - email: 'john.doe@example.com', - firstName: 'John', - lastName: 'Doe', - oidcSub: '1234', - oauthIssuer: 'keycloack', + +const member = { + id: 1, + email: 'john.doe@example.com', + firstName: 'John', + lastName: 'Doe', + oidcSub: 'test-1234', + oauthIssuer: 'keycloack', +}; + +test('Should produce valid user id with prefix', async () => { + const result = await produceSynapseUserId(member, undefined, false); + expect(result).toBe(`@${member.oidcSub}:${serverName}`); +}); + +test('Should produce valid user id without prefix', async () => { + const result = await produceSynapseUserId(member, undefined, true); + expect(result).toBe(`${member.oidcSub}`); +}); + +test('Should produce valid user id with special characters', async () => { + const memberWithSpecialCharsInOidcSub = { + ...member, + oidcSub: 'SomeSpecialCharsà', }; - const result = await produceSynapseUserId(member); - expect(result).toBe('@1234:serverName'); + const expectedOidcSub = 'somespecialcharsa'; + + const result = await produceSynapseUserId(memberWithSpecialCharsInOidcSub); + + expect(result).toBe(`@${expectedOidcSub}:${serverName}`); }); diff --git a/src/services/synapse/produceSynapseUserId.ts b/src/services/synapse/produceSynapseUserId.ts index 6784c6e5..18b6341a 100644 --- a/src/services/synapse/produceSynapseUserId.ts +++ b/src/services/synapse/produceSynapseUserId.ts @@ -13,29 +13,31 @@ export async function produceSynapseUserId( synapseService?: SynapseService, skipPrePostfix: boolean = false ): Promise { + // NOTE: It normalize the oidcSub to replace special characters and make it lowercase + // This is done to enture that the oidcSub can be used as a valid synapse user id + const normalizedMember = { + ...member, + oidcSub: member.oidcSub + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toLowerCase(), + }; + if (synapseService) { - const userIdByOidcSub = await synapseService.getUserByOidcSub(member); - const userIdByEmail = await synapseService.getUserByEmail(member.email); + const user = + (await synapseService.getUserByOidcSub(normalizedMember.oidcSub)) || + (await synapseService.getUserByEmail(normalizedMember.email)); - if (userIdByOidcSub) { - return skipPrePostfix - ? userIdByOidcSub.user_id.replace(/^@|:ess$/g, '') - : userIdByOidcSub.user_id; - } - if (userIdByEmail) { + if (user) { return skipPrePostfix - ? userIdByEmail.user_id.replace(/^@|:ess$/g, '') - : userIdByEmail.user_id; + ? user.user_id.replace(/^@|:ess$/g, '') + : user.user_id; } } - const normalizedId = member.oidcSub - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, ''); - if (skipPrePostfix) { - return normalizedId; + return normalizedMember.oidcSub; } - return `@${normalizedId}:${serverName}`; + return `@${normalizedMember.oidcSub}:${serverName}`; }