diff --git a/.editorconfig b/.editorconfig index d6eafe8d8f..7e5ce6236a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -50,3 +50,9 @@ indent_size = 2 [ietf/**.html] insert_final_newline = false + +# Settings for Kubernetes yaml +# --------------------------------------------------------- +# Use 2-space indents +[k8s/**.yaml] +indent_size = 2 diff --git a/.github/workflows/build-base-app.yml b/.github/workflows/build-base-app.yml index 85842d9dcf..3995b4b49b 100644 --- a/.github/workflows/build-base-app.yml +++ b/.github/workflows/build-base-app.yml @@ -34,7 +34,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build & Push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_NO_SUMMARY: true with: context: . file: docker/base.Dockerfile diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml index 9c37d02ce8..d14e4f2c8b 100644 --- a/.github/workflows/build-celery-worker.yml +++ b/.github/workflows/build-celery-worker.yml @@ -35,7 +35,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build & Push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_NO_SUMMARY: true with: context: . file: dev/celery/Dockerfile diff --git a/.github/workflows/build-mq-broker.yml b/.github/workflows/build-mq-broker.yml index ba935405f9..8c6f1e6ae1 100644 --- a/.github/workflows/build-mq-broker.yml +++ b/.github/workflows/build-mq-broker.yml @@ -37,7 +37,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build & Push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_NO_SUMMARY: true with: context: . file: dev/mq/Dockerfile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d1cb690dc6..afb5951aca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,15 @@ on: workflow_dispatch: inputs: - summary: - description: 'Release Summary' - required: false - type: string - default: '' + deploy: + description: 'Deploy to K8S' + default: 'Skip' + required: true + type: choice + options: + - Skip + - Staging Only + - Staging + Prod sandbox: description: 'Deploy to Sandbox' default: true @@ -22,16 +26,16 @@ on: default: false required: true type: boolean - legacySandbox: - description: 'Deploy to Legacy Sandbox' - default: false - required: false - type: boolean skiptests: description: 'Skip Tests' default: false required: true type: boolean + skiparm: + description: 'Skip ARM64 Build' + default: false + required: true + type: boolean ignoreLowerCoverage: description: 'Ignore Lower Coverage' default: false @@ -161,7 +165,7 @@ jobs: - name: Download a Coverage Results if: ${{ github.event.inputs.skiptests == 'false' || github.ref_name == 'release' }} - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.8 with: name: coverage @@ -201,8 +205,8 @@ jobs: - name: Collect + Push Statics env: DEBIAN_FRONTEND: noninteractive - AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_KEY_SECRET }} + AWS_ACCESS_KEY_ID: ${{ secrets.CF_R2_STATIC_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_R2_STATIC_KEY_SECRET }} AWS_DEFAULT_REGION: auto AWS_ENDPOINT_URL: ${{ secrets.CF_R2_ENDPOINT }} run: | @@ -220,7 +224,7 @@ jobs: .devcontainer .github .vscode - helm + k8s playwright svn-history docker-compose.yml @@ -240,13 +244,17 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build Release Docker Image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_SUMMARY: false with: context: . file: dev/build/Dockerfile - platforms: linux/amd64,linux/arm64 + platforms: ${{ github.event.inputs.skiparm == 'true' && 'linux/amd64' || 'linux/amd64,linux/arm64' }} push: true tags: ghcr.io/ietf-tools/datatracker:${{ env.PKG_VERSION }} + cache-from: type=gha + cache-to: type=gha,mode=max - name: Update CHANGELOG id: changelog @@ -273,7 +281,7 @@ jobs: repoCommon: common version: ${{needs.prepare.outputs.pkg_version}} changelog: ${{ steps.changelog.outputs.changes }} - summary: ${{ github.event.inputs.summary }} + summary: '' coverageResultsPath: coverage.json histCoveragePath: historical-coverage.json @@ -323,7 +331,7 @@ jobs: steps: - name: Notify on Slack (Success) if: ${{ !contains(join(needs.*.result, ','), 'failure') }} - uses: slackapi/slack-github-action@v1.25.0 + uses: slackapi/slack-github-action@v1.26.0 with: channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }} payload: | @@ -346,7 +354,7 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_GH_BOT }} - name: Notify on Slack (Failure) if: ${{ contains(join(needs.*.result, ','), 'failure') }} - uses: slackapi/slack-github-action@v1.25.0 + uses: slackapi/slack-github-action@v1.26.0 with: channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }} payload: | @@ -385,7 +393,7 @@ jobs: - uses: actions/checkout@v4 - name: Download a Release Artifact - uses: actions/download-artifact@v4.1.4 + uses: actions/download-artifact@v4.1.8 with: name: release-${{ env.PKG_VERSION }} @@ -407,62 +415,57 @@ jobs: DEBIAN_FRONTEND: noninteractive run: | docker image prune -a -f - - legacySandbox: - name: Deploy to Legacy Sandbox - if: ${{ !failure() && !cancelled() && github.event.inputs.legacySandbox == 'true' }} + + # ----------------------------------------------------------------- + # STAGING + # ----------------------------------------------------------------- + staging: + name: Deploy to Staging + if: ${{ !failure() && !cancelled() && (github.event.inputs.deploy == 'Staging Only' || github.event.inputs.deploy == 'Staging + Prod' || github.ref_name == 'release') }} needs: [prepare, release] - runs-on: [self-hosted, legacy-sandbox-server] + runs-on: ubuntu-latest environment: - name: legacy-sandbox - url: "https://sandbox.ietf.org" + name: staging env: PKG_VERSION: ${{needs.prepare.outputs.pkg_version}} steps: - - name: Download a Release Artifact - uses: actions/download-artifact@v4.1.4 - with: - name: release-${{ env.PKG_VERSION }} - path: /a/www/ietf-datatracker/main.dev.${{ github.run_number }} + - name: Deploy to staging + uses: the-actions-org/workflow-dispatch@v4 + with: + workflow: deploy.yml + repo: ietf-tools/infra-k8s + ref: main + token: ${{ secrets.GH_INFRA_K8S_TOKEN }} + inputs: '{ "environment":"${{ secrets.GHA_K8S_CLUSTER }}", "app":"datatracker", "appVersion":"${{ env.PKG_VERSION }}", "remoteRef":"${{ github.sha }}" }' + wait-for-completion: true + wait-for-completion-timeout: 10m + wait-for-completion-interval: 30s + display-workflow-run-url: false - - name: Extract Release - env: - DEBIAN_FRONTEND: noninteractive - working-directory: /a/www/ietf-datatracker/main.dev.${{ github.run_number }} - run: | - echo "Extracting release tarball..." - tar xzf release.tar.gz - echo "Deleting release tarball..." - rm -rf release.tar.gz - - - name: Setup Environment - env: - DEBIAN_FRONTEND: noninteractive - working-directory: /a/www/ietf-datatracker/main.dev.${{ github.run_number }} - run: | - echo "Copying settings from previous deploy..." - cp ../web/ietf/settings_local.py ietf/ - rsync -a ../web/test/ test/ - echo "Installing Python dependencies..." - python3.9 -mvenv env - source env/bin/activate - pip install -r requirements.txt - pip freeze > frozen-requirements.txt - echo "Collecting static..." - ietf/manage.py collectstatic - echo "Running checks..." - ietf/manage.py check - - - name: Update Docker Containers - env: - DEBIAN_FRONTEND: noninteractive - working-directory: /a/docker/datatracker - run: | - echo "Pulling latest docker images..." - docker image tag ghcr.io/ietf-tools/datatracker-celery:latest datatracker-celery-fallback - docker image tag ghcr.io/ietf-tools/datatracker-mq:latest datatracker-mq-fallback - docker-compose pull - # echo "Shutting down containers..." - # docker-compose down -t 300 - + # ----------------------------------------------------------------- + # PROD + # ----------------------------------------------------------------- + prod: + name: Deploy to Production + if: ${{ !failure() && !cancelled() && (github.event.inputs.deploy == 'Staging + Prod' || github.ref_name == 'release') }} + needs: [prepare, staging] + runs-on: ubuntu-latest + environment: + name: production + env: + PKG_VERSION: ${{needs.prepare.outputs.pkg_version}} + + steps: + - name: Deploy to production + uses: the-actions-org/workflow-dispatch@v4 + with: + workflow: deploy.yml + repo: ietf-tools/infra-k8s + ref: main + token: ${{ secrets.GH_INFRA_K8S_TOKEN }} + inputs: '{ "environment":"${{ secrets.GHA_K8S_CLUSTER }}", "app":"datatracker", "appVersion":"${{ env.PKG_VERSION }}", "remoteRef":"${{ github.sha }}" }' + wait-for-completion: true + wait-for-completion-timeout: 10m + wait-for-completion-interval: 30s + display-workflow-run-url: false diff --git a/.github/workflows/dev-assets-sync-nightly.yml b/.github/workflows/dev-assets-sync-nightly.yml index bfb50bd41e..a7fe67f012 100644 --- a/.github/workflows/dev-assets-sync-nightly.yml +++ b/.github/workflows/dev-assets-sync-nightly.yml @@ -39,7 +39,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Docker Build & Push - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_NO_SUMMARY: true with: context: . file: dev/shared-assets-sync/Dockerfile diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 523d8d9d68..616ffdcc63 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,7 +59,7 @@ jobs: path: geckodriver.log - name: Upload Coverage Results to Codecov - uses: codecov/codecov-action@v4.1.0 + uses: codecov/codecov-action@v4.5.0 with: files: coverage.xml diff --git a/.gitignore b/.gitignore index 4de5e53ac3..c25e6b5bfe 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ datatracker.sublime-workspace /static /tmp-* /.testresult +*.swp *.pyc __pycache__ .yarn/* diff --git a/.pnp.cjs b/.pnp.cjs index 241319adb6..5fcce34d2f 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -49,13 +49,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@rollup/pluginutils", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.1.0"],\ ["@twuni/emojify", "npm:1.0.2"],\ ["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\ + ["@vue/language-plugin-pug", "npm:2.0.7"],\ ["bootstrap", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.3.3"],\ ["bootstrap-icons", "npm:1.11.3"],\ ["browser-fs-access", "npm:0.35.0"],\ ["browserlist", "npm:1.0.1"],\ ["c8", "npm:9.1.0"],\ - ["caniuse-lite", "npm:1.0.30001597"],\ - ["d3", "npm:7.8.5"],\ + ["caniuse-lite", "npm:1.0.30001603"],\ + ["d3", "npm:7.9.0"],\ ["eslint", "npm:8.57.0"],\ ["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\ ["eslint-plugin-cypress", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.15.1"],\ @@ -63,10 +64,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-n", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:16.6.2"],\ ["eslint-plugin-node", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:11.1.0"],\ ["eslint-plugin-promise", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.1"],\ - ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\ + ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.24.0"],\ ["file-saver", "npm:2.0.5"],\ ["highcharts", "npm:11.4.0"],\ - ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\ + ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.18.1"],\ ["ical.js", "npm:1.5.0"],\ ["jquery", "npm:3.7.1"],\ ["jquery-migrate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.1"],\ @@ -84,7 +85,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["pinia", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.1.7"],\ ["pinia-plugin-persist", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:1.0.0"],\ ["pug", "npm:3.0.2"],\ - ["sass", "npm:1.71.1"],\ + ["sass", "npm:1.72.0"],\ ["seedrandom", "npm:3.0.5"],\ ["select2", "npm:4.1.0-rc.0"],\ ["select2-bootstrap-5-theme", "npm:1.3.0"],\ @@ -93,7 +94,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["slugify", "npm:1.6.6"],\ ["sortablejs", "npm:1.15.2"],\ ["vanillajs-datepicker", "npm:1.3.4"],\ - ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\ + ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.3"],\ ["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.21"],\ ["vue-router", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.3.0"],\ ["zxcvbn", "npm:4.4.2"]\ @@ -2431,10 +2432,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3", {\ - "packageLocation": "./.yarn/__virtual__/@sidvind-better-ajv-errors-virtual-6ac4a81dfc/0/cache/@sidvind-better-ajv-errors-npm-2.1.3-e3d1c524a8-949cb805a1.zip/node_modules/@sidvind/better-ajv-errors/",\ + ["virtual:640261ed3b7a9880a388cc504caacf8ea790dd52f1cb31fbc3be445cb2adc6e73fc87097de620863105eb917510145ef2457d30000c7361456ab67ec0b895136#npm:2.1.3", {\ + "packageLocation": "./.yarn/__virtual__/@sidvind-better-ajv-errors-virtual-ff98ba00e3/0/cache/@sidvind-better-ajv-errors-npm-2.1.3-e3d1c524a8-949cb805a1.zip/node_modules/@sidvind/better-ajv-errors/",\ "packageDependencies": [\ - ["@sidvind/better-ajv-errors", "virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3"],\ + ["@sidvind/better-ajv-errors", "virtual:640261ed3b7a9880a388cc504caacf8ea790dd52f1cb31fbc3be445cb2adc6e73fc87097de620863105eb917510145ef2457d30000c7361456ab67ec0b895136#npm:2.1.3"],\ ["@babel/code-frame", "npm:7.16.7"],\ ["@types/ajv", null],\ ["ajv", "npm:8.11.0"],\ @@ -2709,7 +2710,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\ ["@types/vite", null],\ ["@types/vue", null],\ - ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\ + ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.3"],\ ["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.21"]\ ],\ "packagePeers": [\ @@ -2721,6 +2722,48 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@volar/language-core", [\ + ["npm:2.1.4", {\ + "packageLocation": "./.yarn/cache/@volar-language-core-npm-2.1.4-18ee1a037d-7430f65143.zip/node_modules/@volar/language-core/",\ + "packageDependencies": [\ + ["@volar/language-core", "npm:2.1.4"],\ + ["@volar/source-map", "npm:2.1.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@volar/language-service", [\ + ["npm:2.1.4", {\ + "packageLocation": "./.yarn/cache/@volar-language-service-npm-2.1.4-2d34cb628f-06cdcfacf0.zip/node_modules/@volar/language-service/",\ + "packageDependencies": [\ + ["@volar/language-service", "npm:2.1.4"],\ + ["@volar/language-core", "npm:2.1.4"],\ + ["vscode-languageserver-protocol", "npm:3.17.5"],\ + ["vscode-languageserver-textdocument", "npm:1.0.11"],\ + ["vscode-uri", "npm:3.0.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@volar/source-map", [\ + ["npm:2.1.4", {\ + "packageLocation": "./.yarn/cache/@volar-source-map-npm-2.1.4-5963b1701f-e2f65bcfd6.zip/node_modules/@volar/source-map/",\ + "packageDependencies": [\ + ["@volar/source-map", "npm:2.1.4"],\ + ["muggle-string", "npm:0.4.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@vscode/l10n", [\ + ["npm:0.0.18", {\ + "packageLocation": "./.yarn/cache/@vscode-l10n-npm-0.0.18-8a12efe4b5-c33876cebd.zip/node_modules/@vscode/l10n/",\ + "packageDependencies": [\ + ["@vscode/l10n", "npm:0.0.18"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@vue/compiler-core", [\ ["npm:3.4.21", {\ "packageLocation": "./.yarn/cache/@vue-compiler-core-npm-3.4.21-ec7f24d7f5-0d6b7732bc.zip/node_modules/@vue/compiler-core/",\ @@ -2791,6 +2834,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["@vue/language-plugin-pug", [\ + ["npm:2.0.7", {\ + "packageLocation": "./.yarn/cache/@vue-language-plugin-pug-npm-2.0.7-547300c7e0-11cc96eb5f.zip/node_modules/@vue/language-plugin-pug/",\ + "packageDependencies": [\ + ["@vue/language-plugin-pug", "npm:2.0.7"],\ + ["@volar/source-map", "npm:2.1.4"],\ + ["volar-service-pug", "npm:0.0.34"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@vue/reactivity", [\ ["npm:3.4.21", {\ "packageLocation": "./.yarn/cache/@vue-reactivity-npm-3.4.21-fd3e254d08-79c7ebe3ec.zip/node_modules/@vue/reactivity/",\ @@ -3452,10 +3506,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.0.30001597", {\ - "packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001597-1e349680d5-ec6a2cf0fd.zip/node_modules/caniuse-lite/",\ + ["npm:1.0.30001603", {\ + "packageLocation": "./.yarn/cache/caniuse-lite-npm-1.0.30001603-77af81f60b-e66e0d24b8.zip/node_modules/caniuse-lite/",\ "packageDependencies": [\ - ["caniuse-lite", "npm:1.0.30001597"]\ + ["caniuse-lite", "npm:1.0.30001603"]\ ],\ "linkType": "HARD"\ }]\ @@ -3764,10 +3818,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["d3", [\ - ["npm:7.8.5", {\ - "packageLocation": "./.yarn/cache/d3-npm-7.8.5-5db20a5616-e407e79731.zip/node_modules/d3/",\ + ["npm:7.9.0", {\ + "packageLocation": "./.yarn/cache/d3-npm-7.9.0-d293821ce6-1c0e9135f1.zip/node_modules/d3/",\ "packageDependencies": [\ - ["d3", "npm:7.8.5"],\ + ["d3", "npm:7.9.0"],\ ["d3-array", "npm:3.1.6"],\ ["d3-axis", "npm:3.0.0"],\ ["d3-brush", "npm:3.0.0"],\ @@ -5041,25 +5095,26 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["eslint-plugin-vue", [\ - ["npm:9.22.0", {\ - "packageLocation": "./.yarn/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip/node_modules/eslint-plugin-vue/",\ + ["npm:9.24.0", {\ + "packageLocation": "./.yarn/cache/eslint-plugin-vue-npm-9.24.0-4c6dba51bf-2309b919d8.zip/node_modules/eslint-plugin-vue/",\ "packageDependencies": [\ - ["eslint-plugin-vue", "npm:9.22.0"]\ + ["eslint-plugin-vue", "npm:9.24.0"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0", {\ - "packageLocation": "./.yarn/__virtual__/eslint-plugin-vue-virtual-2da7be1523/0/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip/node_modules/eslint-plugin-vue/",\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.24.0", {\ + "packageLocation": "./.yarn/__virtual__/eslint-plugin-vue-virtual-e080dd5dc6/0/cache/eslint-plugin-vue-npm-9.24.0-4c6dba51bf-2309b919d8.zip/node_modules/eslint-plugin-vue/",\ "packageDependencies": [\ - ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\ + ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.24.0"],\ ["@eslint-community/eslint-utils", "virtual:4286e12a3a0f74af013bc8f16c6d8fdde823cfbf6389660266b171e551f576c805b0a7a8eb2a7087a5cee7dfe6ebb6e1ea3808d93daf915edc95656907a381bb#npm:4.4.0"],\ ["@types/eslint", null],\ ["eslint", "npm:8.57.0"],\ + ["globals", "npm:13.24.0"],\ ["natural-compare", "npm:1.4.0"],\ ["nth-check", "npm:2.1.1"],\ ["postcss-selector-parser", "npm:6.0.15"],\ ["semver", "npm:7.6.0"],\ - ["vue-eslint-parser", "virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2"],\ + ["vue-eslint-parser", "virtual:e080dd5dc65fb3541eb98fd929c3a1d3733f3aff4bb24b09a6b5cce9fba4a29aca07e286ef93079f2144caa0fd33bb6545549286d3a9f2b9a211caa1f4b68ff9#npm:9.4.2"],\ ["xml-name-validator", "npm:4.0.0"]\ ],\ "packagePeers": [\ @@ -5774,20 +5829,20 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["html-validate", [\ - ["npm:8.15.0", {\ - "packageLocation": "./.yarn/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip/node_modules/html-validate/",\ + ["npm:8.18.1", {\ + "packageLocation": "./.yarn/cache/html-validate-npm-8.18.1-c5271a0fb9-53479bf75b.zip/node_modules/html-validate/",\ "packageDependencies": [\ - ["html-validate", "npm:8.15.0"]\ + ["html-validate", "npm:8.18.1"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0", {\ - "packageLocation": "./.yarn/__virtual__/html-validate-virtual-2a2a921469/0/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip/node_modules/html-validate/",\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.18.1", {\ + "packageLocation": "./.yarn/__virtual__/html-validate-virtual-640261ed3b/0/cache/html-validate-npm-8.18.1-c5271a0fb9-53479bf75b.zip/node_modules/html-validate/",\ "packageDependencies": [\ - ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\ + ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.18.1"],\ ["@babel/code-frame", "npm:7.16.7"],\ ["@html-validate/stylish", "npm:4.1.0"],\ - ["@sidvind/better-ajv-errors", "virtual:2a2a921469e6f0bfdb6b2bd79f75a3395d47a481854507365048f3d989418f207cf814cb2ce1a012d2da774c1d130b4ca418582463ec08381da55e543b959c4c#npm:2.1.3"],\ + ["@sidvind/better-ajv-errors", "virtual:640261ed3b7a9880a388cc504caacf8ea790dd52f1cb31fbc3be445cb2adc6e73fc87097de620863105eb917510145ef2457d30000c7361456ab67ec0b895136#npm:2.1.3"],\ ["@types/jest", null],\ ["@types/jest-diff", null],\ ["@types/jest-snapshot", null],\ @@ -7153,6 +7208,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["muggle-string", [\ + ["npm:0.4.1", {\ + "packageLocation": "./.yarn/cache/muggle-string-npm-0.4.1-fe3c825cc2-85fe1766d1.zip/node_modules/muggle-string/",\ + "packageDependencies": [\ + ["muggle-string", "npm:0.4.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["murmurhash-js", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/murmurhash-js-npm-1.0.0-b1fa804bc0-083cea92a1.zip/node_modules/murmurhash-js/",\ @@ -8269,13 +8333,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@rollup/pluginutils", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.1.0"],\ ["@twuni/emojify", "npm:1.0.2"],\ ["@vitejs/plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.6.2"],\ + ["@vue/language-plugin-pug", "npm:2.0.7"],\ ["bootstrap", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.3.3"],\ ["bootstrap-icons", "npm:1.11.3"],\ ["browser-fs-access", "npm:0.35.0"],\ ["browserlist", "npm:1.0.1"],\ ["c8", "npm:9.1.0"],\ - ["caniuse-lite", "npm:1.0.30001597"],\ - ["d3", "npm:7.8.5"],\ + ["caniuse-lite", "npm:1.0.30001603"],\ + ["d3", "npm:7.9.0"],\ ["eslint", "npm:8.57.0"],\ ["eslint-config-standard", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:17.1.0"],\ ["eslint-plugin-cypress", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.15.1"],\ @@ -8283,10 +8348,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["eslint-plugin-n", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:16.6.2"],\ ["eslint-plugin-node", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:11.1.0"],\ ["eslint-plugin-promise", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.1"],\ - ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.22.0"],\ + ["eslint-plugin-vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:9.24.0"],\ ["file-saver", "npm:2.0.5"],\ ["highcharts", "npm:11.4.0"],\ - ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.15.0"],\ + ["html-validate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:8.18.1"],\ ["ical.js", "npm:1.5.0"],\ ["jquery", "npm:3.7.1"],\ ["jquery-migrate", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.1"],\ @@ -8304,7 +8369,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["pinia", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:2.1.7"],\ ["pinia-plugin-persist", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:1.0.0"],\ ["pug", "npm:3.0.2"],\ - ["sass", "npm:1.71.1"],\ + ["sass", "npm:1.72.0"],\ ["seedrandom", "npm:3.0.5"],\ ["select2", "npm:4.1.0-rc.0"],\ ["select2-bootstrap-5-theme", "npm:1.3.0"],\ @@ -8313,7 +8378,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["slugify", "npm:1.6.6"],\ ["sortablejs", "npm:1.15.2"],\ ["vanillajs-datepicker", "npm:1.3.4"],\ - ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\ + ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.3"],\ ["vue", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:3.4.21"],\ ["vue-router", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.3.0"],\ ["zxcvbn", "npm:4.4.2"]\ @@ -8401,10 +8466,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "HARD"\ }],\ - ["npm:1.71.1", {\ - "packageLocation": "./.yarn/cache/sass-npm-1.71.1-3aced13991-19c4939d30.zip/node_modules/sass/",\ + ["npm:1.72.0", {\ + "packageLocation": "./.yarn/cache/sass-npm-1.72.0-fb38bb530c-f420079c7d.zip/node_modules/sass/",\ "packageDependencies": [\ - ["sass", "npm:1.71.1"],\ + ["sass", "npm:1.72.0"],\ ["chokidar", "npm:3.5.3"],\ ["immutable", "npm:4.0.0"],\ ["source-map-js", "npm:1.0.2"]\ @@ -9203,17 +9268,17 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["vite", [\ - ["npm:4.5.2", {\ - "packageLocation": "./.yarn/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip/node_modules/vite/",\ + ["npm:4.5.3", {\ + "packageLocation": "./.yarn/cache/vite-npm-4.5.3-5cedc7cb8f-fd3f512ce4.zip/node_modules/vite/",\ "packageDependencies": [\ - ["vite", "npm:4.5.2"]\ + ["vite", "npm:4.5.3"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2", {\ - "packageLocation": "./.yarn/__virtual__/vite-virtual-8f548b7c00/0/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip/node_modules/vite/",\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.3", {\ + "packageLocation": "./.yarn/__virtual__/vite-virtual-69c30fd9fd/0/cache/vite-npm-4.5.3-5cedc7cb8f-fd3f512ce4.zip/node_modules/vite/",\ "packageDependencies": [\ - ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.2"],\ + ["vite", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:4.5.3"],\ ["@types/less", null],\ ["@types/lightningcss", null],\ ["@types/node", null],\ @@ -9227,7 +9292,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["lightningcss", null],\ ["postcss", "npm:8.4.33"],\ ["rollup", "npm:3.29.4"],\ - ["sass", "npm:1.71.1"],\ + ["sass", "npm:1.72.0"],\ ["stylus", null],\ ["sugarss", null],\ ["terser", null]\ @@ -9259,6 +9324,46 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["volar-service-html", [\ + ["npm:0.0.34", {\ + "packageLocation": "./.yarn/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip/node_modules/volar-service-html/",\ + "packageDependencies": [\ + ["volar-service-html", "npm:0.0.34"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34", {\ + "packageLocation": "./.yarn/__virtual__/volar-service-html-virtual-5a9107a24d/0/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip/node_modules/volar-service-html/",\ + "packageDependencies": [\ + ["volar-service-html", "virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34"],\ + ["@types/volar__language-service", null],\ + ["@volar/language-service", "npm:2.1.4"],\ + ["vscode-html-languageservice", "npm:5.1.2"],\ + ["vscode-languageserver-textdocument", "npm:1.0.11"],\ + ["vscode-uri", "npm:3.0.8"]\ + ],\ + "packagePeers": [\ + "@types/volar__language-service",\ + "@volar/language-service"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["volar-service-pug", [\ + ["npm:0.0.34", {\ + "packageLocation": "./.yarn/cache/volar-service-pug-npm-0.0.34-6f5429e17c-4691aa1c8e.zip/node_modules/volar-service-pug/",\ + "packageDependencies": [\ + ["volar-service-pug", "npm:0.0.34"],\ + ["@volar/language-service", "npm:2.1.4"],\ + ["pug-lexer", "npm:5.0.1"],\ + ["pug-parser", "npm:6.0.0"],\ + ["volar-service-html", "virtual:6f5429e17c4ecd390af605a4e97ecc7b34f2f1374a5e30c21f0a978cbdc904738a42d0d6f5d44d2e969250218b3c205853d6afefd88b87bcda877286d12bef83#npm:0.0.34"],\ + ["vscode-html-languageservice", "npm:5.1.2"],\ + ["vscode-languageserver-textdocument", "npm:1.0.11"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vooks", [\ ["npm:0.2.12", {\ "packageLocation": "./.yarn/cache/vooks-npm-0.2.12-0d1a2d856b-e6841ec5b6.zip/node_modules/vooks/",\ @@ -9282,6 +9387,66 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["vscode-html-languageservice", [\ + ["npm:5.1.2", {\ + "packageLocation": "./.yarn/cache/vscode-html-languageservice-npm-5.1.2-2ea2618bdd-3a2a5ee5ad.zip/node_modules/vscode-html-languageservice/",\ + "packageDependencies": [\ + ["vscode-html-languageservice", "npm:5.1.2"],\ + ["@vscode/l10n", "npm:0.0.18"],\ + ["vscode-languageserver-textdocument", "npm:1.0.11"],\ + ["vscode-languageserver-types", "npm:3.17.5"],\ + ["vscode-uri", "npm:3.0.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["vscode-jsonrpc", [\ + ["npm:8.2.0", {\ + "packageLocation": "./.yarn/cache/vscode-jsonrpc-npm-8.2.0-b7d2e5b553-f302a01e59.zip/node_modules/vscode-jsonrpc/",\ + "packageDependencies": [\ + ["vscode-jsonrpc", "npm:8.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["vscode-languageserver-protocol", [\ + ["npm:3.17.5", {\ + "packageLocation": "./.yarn/cache/vscode-languageserver-protocol-npm-3.17.5-2b07e16989-dfb42d276d.zip/node_modules/vscode-languageserver-protocol/",\ + "packageDependencies": [\ + ["vscode-languageserver-protocol", "npm:3.17.5"],\ + ["vscode-jsonrpc", "npm:8.2.0"],\ + ["vscode-languageserver-types", "npm:3.17.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["vscode-languageserver-textdocument", [\ + ["npm:1.0.11", {\ + "packageLocation": "./.yarn/cache/vscode-languageserver-textdocument-npm-1.0.11-6fc94d2b7b-ea7cdc9d4f.zip/node_modules/vscode-languageserver-textdocument/",\ + "packageDependencies": [\ + ["vscode-languageserver-textdocument", "npm:1.0.11"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["vscode-languageserver-types", [\ + ["npm:3.17.5", {\ + "packageLocation": "./.yarn/cache/vscode-languageserver-types-npm-3.17.5-aca3b71a5a-79b420e757.zip/node_modules/vscode-languageserver-types/",\ + "packageDependencies": [\ + ["vscode-languageserver-types", "npm:3.17.5"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["vscode-uri", [\ + ["npm:3.0.8", {\ + "packageLocation": "./.yarn/cache/vscode-uri-npm-3.0.8-56f46b9d24-5142491268.zip/node_modules/vscode-uri/",\ + "packageDependencies": [\ + ["vscode-uri", "npm:3.0.8"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["vue", [\ ["npm:3.4.21", {\ "packageLocation": "./.yarn/cache/vue-npm-3.4.21-02110aa6d9-3c477982a0.zip/node_modules/vue/",\ @@ -9367,10 +9532,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ],\ "linkType": "SOFT"\ }],\ - ["virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2", {\ - "packageLocation": "./.yarn/__virtual__/vue-eslint-parser-virtual-989a31128c/0/cache/vue-eslint-parser-npm-9.4.2-3e4e696025-67f14c8ea1.zip/node_modules/vue-eslint-parser/",\ + ["virtual:e080dd5dc65fb3541eb98fd929c3a1d3733f3aff4bb24b09a6b5cce9fba4a29aca07e286ef93079f2144caa0fd33bb6545549286d3a9f2b9a211caa1f4b68ff9#npm:9.4.2", {\ + "packageLocation": "./.yarn/__virtual__/vue-eslint-parser-virtual-f703c550a2/0/cache/vue-eslint-parser-npm-9.4.2-3e4e696025-67f14c8ea1.zip/node_modules/vue-eslint-parser/",\ "packageDependencies": [\ - ["vue-eslint-parser", "virtual:2da7be1523058e3d6960a64ca4697b767ffeb995ed69da9eb74f6f820005e744683aa6cde73ca784c556dfc97ad5b1bc884e0f3f37187c87e646cf74b01fce0e#npm:9.4.2"],\ + ["vue-eslint-parser", "virtual:e080dd5dc65fb3541eb98fd929c3a1d3733f3aff4bb24b09a6b5cce9fba4a29aca07e286ef93079f2144caa0fd33bb6545549286d3a9f2b9a211caa1f4b68ff9#npm:9.4.2"],\ ["@types/eslint", null],\ ["debug", "virtual:b86a9fb34323a98c6519528ed55faa0d9b44ca8879307c0b29aa384bde47ff59a7d0c9051b31246f14521dfb71ba3c5d6d0b35c29fffc17bf875aa6ad977d9e8#npm:4.3.4"],\ ["eslint", "npm:8.57.0"],\ diff --git a/.yarn/cache/@volar-language-core-npm-2.1.4-18ee1a037d-7430f65143.zip b/.yarn/cache/@volar-language-core-npm-2.1.4-18ee1a037d-7430f65143.zip new file mode 100644 index 0000000000..25e6d3f94d Binary files /dev/null and b/.yarn/cache/@volar-language-core-npm-2.1.4-18ee1a037d-7430f65143.zip differ diff --git a/.yarn/cache/@volar-language-service-npm-2.1.4-2d34cb628f-06cdcfacf0.zip b/.yarn/cache/@volar-language-service-npm-2.1.4-2d34cb628f-06cdcfacf0.zip new file mode 100644 index 0000000000..5f494d902e Binary files /dev/null and b/.yarn/cache/@volar-language-service-npm-2.1.4-2d34cb628f-06cdcfacf0.zip differ diff --git a/.yarn/cache/@volar-source-map-npm-2.1.4-5963b1701f-e2f65bcfd6.zip b/.yarn/cache/@volar-source-map-npm-2.1.4-5963b1701f-e2f65bcfd6.zip new file mode 100644 index 0000000000..0ea96c4d97 Binary files /dev/null and b/.yarn/cache/@volar-source-map-npm-2.1.4-5963b1701f-e2f65bcfd6.zip differ diff --git a/.yarn/cache/@vscode-l10n-npm-0.0.18-8a12efe4b5-c33876cebd.zip b/.yarn/cache/@vscode-l10n-npm-0.0.18-8a12efe4b5-c33876cebd.zip new file mode 100644 index 0000000000..2d6533a204 Binary files /dev/null and b/.yarn/cache/@vscode-l10n-npm-0.0.18-8a12efe4b5-c33876cebd.zip differ diff --git a/.yarn/cache/@vue-language-plugin-pug-npm-2.0.7-547300c7e0-11cc96eb5f.zip b/.yarn/cache/@vue-language-plugin-pug-npm-2.0.7-547300c7e0-11cc96eb5f.zip new file mode 100644 index 0000000000..e637e5f556 Binary files /dev/null and b/.yarn/cache/@vue-language-plugin-pug-npm-2.0.7-547300c7e0-11cc96eb5f.zip differ diff --git a/.yarn/cache/caniuse-lite-npm-1.0.30001597-1e349680d5-ec6a2cf0fd.zip b/.yarn/cache/caniuse-lite-npm-1.0.30001597-1e349680d5-ec6a2cf0fd.zip deleted file mode 100644 index f1545a78ff..0000000000 Binary files a/.yarn/cache/caniuse-lite-npm-1.0.30001597-1e349680d5-ec6a2cf0fd.zip and /dev/null differ diff --git a/.yarn/cache/caniuse-lite-npm-1.0.30001603-77af81f60b-e66e0d24b8.zip b/.yarn/cache/caniuse-lite-npm-1.0.30001603-77af81f60b-e66e0d24b8.zip new file mode 100644 index 0000000000..f3bd2d06bc Binary files /dev/null and b/.yarn/cache/caniuse-lite-npm-1.0.30001603-77af81f60b-e66e0d24b8.zip differ diff --git a/.yarn/cache/d3-npm-7.8.5-5db20a5616-e407e79731.zip b/.yarn/cache/d3-npm-7.8.5-5db20a5616-e407e79731.zip deleted file mode 100644 index b06dd3f260..0000000000 Binary files a/.yarn/cache/d3-npm-7.8.5-5db20a5616-e407e79731.zip and /dev/null differ diff --git a/.yarn/cache/d3-npm-7.9.0-d293821ce6-1c0e9135f1.zip b/.yarn/cache/d3-npm-7.9.0-d293821ce6-1c0e9135f1.zip new file mode 100644 index 0000000000..e78ffffee5 Binary files /dev/null and b/.yarn/cache/d3-npm-7.9.0-d293821ce6-1c0e9135f1.zip differ diff --git a/.yarn/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip b/.yarn/cache/eslint-plugin-vue-npm-9.24.0-4c6dba51bf-2309b919d8.zip similarity index 54% rename from .yarn/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip rename to .yarn/cache/eslint-plugin-vue-npm-9.24.0-4c6dba51bf-2309b919d8.zip index b0eff51136..285d11da2d 100644 Binary files a/.yarn/cache/eslint-plugin-vue-npm-9.22.0-2cdb92f3c1-5f1e94b412.zip and b/.yarn/cache/eslint-plugin-vue-npm-9.24.0-4c6dba51bf-2309b919d8.zip differ diff --git a/.yarn/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip b/.yarn/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip deleted file mode 100644 index fed180abd3..0000000000 Binary files a/.yarn/cache/html-validate-npm-8.15.0-a1dfa4198d-0af7685ca1.zip and /dev/null differ diff --git a/.yarn/cache/html-validate-npm-8.18.1-c5271a0fb9-53479bf75b.zip b/.yarn/cache/html-validate-npm-8.18.1-c5271a0fb9-53479bf75b.zip new file mode 100644 index 0000000000..b2f855af03 Binary files /dev/null and b/.yarn/cache/html-validate-npm-8.18.1-c5271a0fb9-53479bf75b.zip differ diff --git a/.yarn/cache/muggle-string-npm-0.4.1-fe3c825cc2-85fe1766d1.zip b/.yarn/cache/muggle-string-npm-0.4.1-fe3c825cc2-85fe1766d1.zip new file mode 100644 index 0000000000..4cec1b177d Binary files /dev/null and b/.yarn/cache/muggle-string-npm-0.4.1-fe3c825cc2-85fe1766d1.zip differ diff --git a/.yarn/cache/sass-npm-1.71.1-3aced13991-19c4939d30.zip b/.yarn/cache/sass-npm-1.71.1-3aced13991-19c4939d30.zip deleted file mode 100644 index f1ee7f94d2..0000000000 Binary files a/.yarn/cache/sass-npm-1.71.1-3aced13991-19c4939d30.zip and /dev/null differ diff --git a/.yarn/cache/sass-npm-1.72.0-fb38bb530c-f420079c7d.zip b/.yarn/cache/sass-npm-1.72.0-fb38bb530c-f420079c7d.zip new file mode 100644 index 0000000000..a3aea4e668 Binary files /dev/null and b/.yarn/cache/sass-npm-1.72.0-fb38bb530c-f420079c7d.zip differ diff --git a/.yarn/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip b/.yarn/cache/vite-npm-4.5.3-5cedc7cb8f-fd3f512ce4.zip similarity index 68% rename from .yarn/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip rename to .yarn/cache/vite-npm-4.5.3-5cedc7cb8f-fd3f512ce4.zip index 3ba408d602..c6bb0e4ef7 100644 Binary files a/.yarn/cache/vite-npm-4.5.2-e430b2c117-9d1f84f703.zip and b/.yarn/cache/vite-npm-4.5.3-5cedc7cb8f-fd3f512ce4.zip differ diff --git a/.yarn/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip b/.yarn/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip new file mode 100644 index 0000000000..0f1e9805f7 Binary files /dev/null and b/.yarn/cache/volar-service-html-npm-0.0.34-32b6d24136-83b50cd805.zip differ diff --git a/.yarn/cache/volar-service-pug-npm-0.0.34-6f5429e17c-4691aa1c8e.zip b/.yarn/cache/volar-service-pug-npm-0.0.34-6f5429e17c-4691aa1c8e.zip new file mode 100644 index 0000000000..d53f3521ee Binary files /dev/null and b/.yarn/cache/volar-service-pug-npm-0.0.34-6f5429e17c-4691aa1c8e.zip differ diff --git a/.yarn/cache/vscode-html-languageservice-npm-5.1.2-2ea2618bdd-3a2a5ee5ad.zip b/.yarn/cache/vscode-html-languageservice-npm-5.1.2-2ea2618bdd-3a2a5ee5ad.zip new file mode 100644 index 0000000000..d83607888b Binary files /dev/null and b/.yarn/cache/vscode-html-languageservice-npm-5.1.2-2ea2618bdd-3a2a5ee5ad.zip differ diff --git a/.yarn/cache/vscode-jsonrpc-npm-8.2.0-b7d2e5b553-f302a01e59.zip b/.yarn/cache/vscode-jsonrpc-npm-8.2.0-b7d2e5b553-f302a01e59.zip new file mode 100644 index 0000000000..75e2c086b6 Binary files /dev/null and b/.yarn/cache/vscode-jsonrpc-npm-8.2.0-b7d2e5b553-f302a01e59.zip differ diff --git a/.yarn/cache/vscode-languageserver-protocol-npm-3.17.5-2b07e16989-dfb42d276d.zip b/.yarn/cache/vscode-languageserver-protocol-npm-3.17.5-2b07e16989-dfb42d276d.zip new file mode 100644 index 0000000000..bcb5ae5b4e Binary files /dev/null and b/.yarn/cache/vscode-languageserver-protocol-npm-3.17.5-2b07e16989-dfb42d276d.zip differ diff --git a/.yarn/cache/vscode-languageserver-textdocument-npm-1.0.11-6fc94d2b7b-ea7cdc9d4f.zip b/.yarn/cache/vscode-languageserver-textdocument-npm-1.0.11-6fc94d2b7b-ea7cdc9d4f.zip new file mode 100644 index 0000000000..b1edfda12d Binary files /dev/null and b/.yarn/cache/vscode-languageserver-textdocument-npm-1.0.11-6fc94d2b7b-ea7cdc9d4f.zip differ diff --git a/.yarn/cache/vscode-languageserver-types-npm-3.17.5-aca3b71a5a-79b420e757.zip b/.yarn/cache/vscode-languageserver-types-npm-3.17.5-aca3b71a5a-79b420e757.zip new file mode 100644 index 0000000000..ec214b2903 Binary files /dev/null and b/.yarn/cache/vscode-languageserver-types-npm-3.17.5-aca3b71a5a-79b420e757.zip differ diff --git a/.yarn/cache/vscode-uri-npm-3.0.8-56f46b9d24-5142491268.zip b/.yarn/cache/vscode-uri-npm-3.0.8-56f46b9d24-5142491268.zip new file mode 100644 index 0000000000..6dadd110c9 Binary files /dev/null and b/.yarn/cache/vscode-uri-npm-3.0.8-56f46b9d24-5142491268.zip differ diff --git a/README.md b/README.md index 20c9f3e599..af059f972b 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ npm run install-deps npm run test:legacy ``` + ### Diff Tool To compare 2 different datatracker instances and look for diff, read the [diff tool instructions](dev/diff). diff --git a/bin/check-copyright b/bin/check-copyright deleted file mode 100755 index 13cbcd8582..0000000000 --- a/bin/check-copyright +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3.7 -# -*- mode: python; coding: utf-8 -*- -# Copyright The IETF Trust 2019, All Rights Reserved -""" -NAME - $program - Check for current copyright notice in given files - -SYNOPSIS - $program [OPTIONS] ARGS - -DESCRIPTION - Given a list of files or filename wildcard patterns, check all for - an IETF Trust copyright notice with the current year. Optionally - generate a diff on standard out which can be used by 'patch'. - - An invocation similar to the following can be particularly useful with - a set of changed version-controlled files, as it will fix up the - Copyright statements of any python files with pending changes: - - $ check-copyright -p $(svn st | cut -c 9- | grep '\.py$' ) | patch -p0 - - -%(options)s - -AUTHOR - Written by Henrik Levkowetz, - -COPYRIGHT - Copyright 2019 the IETF Trust - - This program is free software; you can redistribute it and/or modify - it under the terms of the Simplified BSD license as published by the - Open Source Initiative at http://opensource.org/licenses/BSD-2-Clause. - -""" - - -import datetime -import os -import sys -import time - -path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -if not path in sys.path: - sys.path.insert(0, path) - -import getopt -import re -import pytz -import tzparse -import debug - -version = "1.0.0" -program = os.path.basename(sys.argv[0]) -progdir = os.path.dirname(sys.argv[0]) - -debug.debug = True - -# ---------------------------------------------------------------------- -# Parse options - -options = "" -for line in re.findall("\n +(if|elif) +opt in \[(.+)\]:\s+#(.+)\n", open(sys.argv[0]).read()): - if not options: - options += "OPTIONS\n" - options += " %-16s %s\n" % (line[1].replace('"', ''), line[2]) -options = options.strip() - -# with ' < 1:' on the next line, this is a no-op: -if len(sys.argv) < 1: - print(__doc__ % locals()) - sys.exit(1) - -try: - opts, files = getopt.gnu_getopt(sys.argv[1:], "hC:pvV", ["help", "copyright=", "patch", "version", "verbose",]) -except Exception as e: - print( "%s: %s" % (program, e)) - sys.exit(1) - -# ---------------------------------------------------------------------- -# Handle options - -# set default values, if any -opt_verbose = 0 -opt_patch = False -opt_copyright = "Copyright The IETF Trust {years}, All Rights Reserved" - -# handle individual options -for opt, value in opts: - if opt in ["-h", "--help"]: # Output this help, then exit - print( __doc__ % locals() ) - sys.exit(1) - elif opt in ["-p", "--patch"]: # Generate patch output rather than error messages - opt_patch = True - elif opt in ["-C", "--copyright"]: # Copyright line pattern using {years} for years - opt_copyright = value - elif opt in ["-V", "--version"]: # Output version information, then exit - print( program, version ) - sys.exit(0) - elif opt in ["-v", "--verbose"]: # Be more verbose - opt_verbose += 1 - -# ---------------------------------------------------------------------- -def say(s): - sys.stderr.write("%s\n" % (s)) - -# ---------------------------------------------------------------------- -def note(s): - if opt_verbose: - sys.stderr.write("%s\n" % (s)) - -# ---------------------------------------------------------------------- -def die(s, error=1): - sys.stderr.write("\n%s: Error: %s\n\n" % (program, s)) - sys.exit(error) - -# ---------------------------------------------------------------------- - -def pipe(cmd, inp=None): - import shlex - from subprocess import Popen, PIPE - args = shlex.split(cmd) - bufsize = 4096 - stdin = PIPE if inp else None - pipe = Popen(args, stdin=stdin, stdout=PIPE, stderr=PIPE, bufsize=bufsize, encoding='utf-8', universal_newlines=True) - out, err = pipe.communicate(inp) - code = pipe.returncode - if code != 0: - raise OSError(err) - return out - -# ---------------------------------------------------------------------- -def split_loginfo(line): - try: - parts = line.split() - rev = parts[0][1:] - who = parts[2] - date = parts[4] - time = parts[5] - tz = parts[6] - when = tzparse.tzparse(" ".join(parts[4:7]), "%Y-%m-%d %H:%M:%S %Z") - when = when.astimezone(pytz.utc) - except ValueError as e: - sys.stderr.write("Bad log line format: %s\n %s\n" % (line, e)) - - return rev, who, when - -# ---------------------------------------------------------------------- -def get_first_commit(path): - note("Getting first commit for '%s'" % path) - cmd = 'svn log %s' % path - if opt_verbose > 1: - note("Running '%s' ..." % cmd) - try: - commit_log = pipe(cmd) - commit_log = commit_log.splitlines() - commit_log.reverse() - for line in commit_log: - if re.search(loginfo_format, line): - rev, who, when = split_loginfo(line) - break - else: - pass - except OSError: - rev, who, when = None, None, datetime.datetime.now(datetime.timezone.utc) - return { path: { 'rev': rev, 'who': who, 'date': when.strftime('%Y-%m-%d %H:%M:%S'), }, } - - -# ---------------------------------------------------------------------- -# The program itself - -import os -import json - -cwd = os.getcwd() - -# Get current initinfo from cache and svn -cachefn = os.path.join(os.environ.get('HOME', '.'), '.initinfo') - -if os.path.exists(cachefn): - note("Reading initinfo cache file %s" % cachefn) - with open(cachefn, "r") as file: - cache = json.load(file) -else: - sys.stderr.write("No initinfo cache file found -- will have to extract all information from SVN.\n"+ - "This may take some time.\n\n") - cache = {} -initinfo = cache - -merged_revs = {} -write_cache = False -loginfo_format = r'^r[0-9]+ \| [^@]+@[^@]+ \| \d\d\d\d-\d\d-\d\d ' - -year = time.strftime('%Y') -copyright_re = "(?i)"+opt_copyright.format(years=r"(\d+-)?\d+") -for path in files: - try: - if not os.path.exists(path): - note("File does not exist: %s" % path) - continue - note("Checking path %s" % path) - if not path in initinfo: - initinfo.update(get_first_commit(path)) - write_cache = True - date = initinfo[path]['date'] - init = date[:4] - - copyright_year_re = "(?i)"+opt_copyright.format(years=r"({init}-)?{year}".format(init=init, year=year)) - with open(path) as file: - try: - chunk = file.read(4000) - except UnicodeDecodeError as e: - sys.stderr.write(f'Error when reading {file.name}: {e}\n') - raise - if os.path.basename(path) == '__init__.py' and len(chunk)==0: - continue - if not re.search(copyright_year_re, chunk): - if year == init: - copyright = opt_copyright.format(years=year) - else: - copyright = opt_copyright.format(years=f"{init}-{year}") - if opt_patch: - print(f"--- {file.name}\t(original)") - print(f"+++ {file.name}\t(modified)") - if not re.search(copyright_re, chunk): - # Simple case, just insert copyright at the top - print( "@@ -1,3 +1,4 @@") - print(f"+# {copyright}") - for i, line in list(enumerate(chunk.splitlines()))[:3]: - print(f" {line}") - else: - # Find old copyright, then emit preceding lines, - # change, and following lines. - pos = None - for i, line in enumerate(chunk.splitlines(), start=1): - if re.search(copyright_re, line): - pos = i - break - if not pos: - raise RuntimeError("Unexpected state: Expected a copyright line, but found none") - print(f"@@ -1,{pos+3} +1,{pos+3} @@") - for i, line in list(enumerate(chunk.splitlines(), start=1))[:pos+3]: - if i == pos: - print(f"-{line}") - print(f"+# {copyright}") - else: - print(f" {line}") - else: - sys.stderr.write(f"{path}(1): Error: Missing or bad copyright. Expected: {copyright}") - except Exception: - if write_cache: - cache = initinfo - with open(cachefn, "w") as file: - json.dump(cache, file, indent=2, sort_keys=True) - raise - -if write_cache: - cache = initinfo - with open(cachefn, "w") as file: - json.dump(cache, file, indent=2, sort_keys=True) - diff --git a/bin/count.c b/bin/count.c deleted file mode 100644 index 786f15eb97..0000000000 --- a/bin/count.c +++ /dev/null @@ -1,26 +0,0 @@ -#include - -int main( void ) -{ - int c; - int count = 0; - - //turn off buffering - setvbuf(stdin, NULL, _IONBF, 0); - setvbuf(stdout, NULL, _IONBF, 0); - setvbuf(stderr, NULL, _IONBF, 0); - - c = fgetc(stdin); - while(c != EOF) - { - if (c=='.' || c=='E' || c=='F' || c=='s') count++; else count=0; - fputc(c, stdout); - fflush(stdout); - if (count && count % 76 == 0) { - fprintf(stderr, "%4d\n", count); - fflush(stderr); - } - c = fgetc(stdin); - } - return 0; -} diff --git a/bin/daily b/bin/daily deleted file mode 100755 index bd33ca2e0b..0000000000 --- a/bin/daily +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# Nightly datatracker jobs. -# -# This script is expected to be triggered by cron from -# /etc/cron.d/datatracker -export LANG=en_US.UTF-8 -export PYTHONIOENCODING=utf-8 - -# Make sure we stop if something goes wrong: -program=${0##*/} -trap 'echo "$program($LINENO): Command failed with error code $? ([$$] $0 $*)"; exit 1' ERR - -# Datatracker directory -DTDIR=/a/www/ietf-datatracker/web -cd $DTDIR/ - -logger -p user.info -t cron "Running $DTDIR/bin/daily" - -# Run the hourly jobs first -$DTDIR/bin/hourly - -# Set up the virtual environment -source $DTDIR/env/bin/activate - - -# Update our information about the current version of some commands we use -$DTDIR/ietf/manage.py update_external_command_info - -# Get IANA-registered yang models -#YANG_IANA_DIR=$(python -c 'import ietf.settings; print ietf.settings.SUBMIT_YANG_IANA_MODEL_DIR') -# Hardcode the rsync target to avoid any unwanted deletes: -# rsync -avzq --delete rsync.ietf.org::iana/yang-parameters/ /a/www/ietf-ftp/yang/ianamod/ -rsync -avzq --delete /a/www/ietf-ftp/iana/yang-parameters/ /a/www/ietf-ftp/yang/ianamod/ - -# Get Yang models from Yangcatalog. -#rsync -avzq rsync://rsync.yangcatalog.org:10873/yangdeps /a/www/ietf-ftp/yang/catalogmod/ -/a/www/ietf-datatracker/scripts/sync_to_yangcatalog - -# Populate the yang repositories -$DTDIR/ietf/manage.py populate_yang_model_dirs -v0 - -# Re-run yang checks on active documents -$DTDIR/ietf/manage.py run_yang_model_checks -v0 - -# Expire last calls -# Enable when removed from /a/www/ietf-datatracker/scripts/Cron-runner: -$DTDIR/ietf/bin/expire-last-calls - -# Send reminders originating from the review app -$DTDIR/ietf/bin/send-review-reminders - -# Purge older PersonApiKeyEvents -$DTDIR/ietf/manage.py purge_old_personal_api_key_events 14 diff --git a/bin/drop-new-tables b/bin/drop-new-tables deleted file mode 100755 index ec1594ae26..0000000000 --- a/bin/drop-new-tables +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Drop tables which don't exist in the database dump. - -[ -n "$1" ] || { echo -e "\nUsage: $0 DUMPFILE\n\nError: No database dump file given"; exit 1; } - -zcat $1 | head | grep "Database: ietf_utf8" || { echo "Is this a database dump? Expected to see 'Database: ietf_utf8' "; exit 1; } - -echo -e "\nSQL commands:\n" - -diff <(zcat $1 | grep '^DROP TABLE IF EXISTS' | tr -d '`;' | field 5) <(ietf/manage.py dbshell <<< 'show tables;' | tail -n +2) | grep '^>' | awk '{print "drop table if exists", $2, ";";}' | tee /dev/stderr | ietf/manage.py dbshell - -echo -e "\nDone" diff --git a/bin/dump-to-names-json b/bin/dump-to-names-json deleted file mode 100644 index 9c7dfac07d..0000000000 --- a/bin/dump-to-names-json +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -# This script provides a limited selected dump of database content with the -# purpose of generating a test fixture that provides the test data needed -# by the test suite. -# -# The generated data fixture is sorted and normalized in order to produce -# minimal commit diffs which reflect only actual changes in the fixture data, -# without apparent changes resulting only from ordering changes. - -set -x -ietf/manage.py dumpdata --indent 1 doc.State doc.BallotType doc.StateType \ - mailtrigger.MailTrigger mailtrigger.Recipient name utils.VersionInfo \ - group.GroupFeatures stats.CountryAlias dbtemplate.DBTemplate \ - | jq --sort-keys "sort_by(.model, .pk)" \ - | jq '[.[] | select(.model!="dbtemplate.dbtemplate" or .pk==354)]' > ietf/name/fixtures/names.json diff --git a/bin/hourly b/bin/hourly deleted file mode 100755 index 9478bec119..0000000000 --- a/bin/hourly +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -# Hourly datatracker jobs -# -# This script is expected to be triggered by cron from -# /etc/cron.d/datatracker -export LANG=en_US.UTF-8 -export PYTHONIOENCODING=utf-8 - -# Make sure we stop if something goes wrong: -program=${0##*/} -trap 'echo "$program($LINENO): Command failed with error code $? ([$$] $0 $*)"; exit 1' ERR - -DTDIR=/a/www/ietf-datatracker/web -cd $DTDIR/ - -# Set up the virtual environment -source $DTDIR/env/bin/activate - -logger -p user.info -t cron "Running $DTDIR/bin/hourly" - -# Generate some static files -ID=/a/ietfdata/doc/draft/repository -DERIVED=/a/ietfdata/derived -DOWNLOAD=/a/www/www6s/download - -$DTDIR/ietf/manage.py generate_idnits2_rfc_status -$DTDIR/ietf/manage.py generate_idnits2_rfcs_obsoleted - -CHARTER=/a/www/ietf-ftp/charter -wget -q https://datatracker.ietf.org/wg/1wg-charters-by-acronym.txt -O $CHARTER/1wg-charters-by-acronym.txt -wget -q https://datatracker.ietf.org/wg/1wg-charters.txt -O $CHARTER/1wg-charters.txt - -# Regenerate the last week of bibxml-ids -$DTDIR/ietf/manage.py generate_draft_bibxml_files - -# Create and update group wikis -#$DTDIR/ietf/manage.py create_group_wikis - -# exit 0 diff --git a/bin/mkdiagram b/bin/mkdiagram deleted file mode 100755 index 4f015c0abe..0000000000 --- a/bin/mkdiagram +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# assume we're in bin/, sibling to ietf/ - -cd ${0%/*}/../ietf || { echo "CD to ietf directory failed, bailing out"; exit; } - -trap 'echo "$program($LINENO): Command failed with error code $? ($0 $*)"; exit 1' ERR - -if [ "$*" ]; then apps="$@"; graph="${1%.*}"; else apps=$(ls */models.py | sed 's!/models.py!!'); graph="models"; fi - -newapps="doc group meeting message person name" -legacyapps="announcements idindex idrfc idtracker iesg ietfauth ipr liaisons mailinglists proceedings redirects submit wgcharter wginfo" - -proxy="$(grep ^class */proxy.py | tr '()' ' ' | awk '{printf $2 ","}')" -names="$(grep ^class name/models.py | tr '()' ' ' | awk '{printf $2 ","}')" -legacy="$(for app in $legacyapps; do grep ^class $app/models.py | tr '()' ' '; done | grep -v ' Meeting\\(' | awk '{printf $2 ","}')" -events="$(egrep '^class .+DocEvent' doc/models.py | tr '()' ' ' | awk '{printf $2 ","}')" - -echo -e "proxy: $proxy\n" -echo -e "names: $names\n" -echo -e "legacy:$legacy\n" -echo -e "events:$events\n" - -exclude="--exclude=$proxy,$names,$legacy" - -export PYTHONPATH="$PWD/.." - -echo "Validating..." -./manage.py validate - -export PYTHONPATH=`dirname $PWD` -module=${PWD##*/} -export DJANGO_SETTINGS_MODULE=$module.settings -export graph -export title - -echo "Generate model graph" -graph="models-with-names-and-events" -title="New IETF Database schema" -${0%/*}/../ietf/manage.py graph_models --exclude="$proxy,$legacy" --title "$title" $apps > $graph.dot && dot -Tpng $graph.dot > $graph.png - -echo "Generate new model without names" -graph="models-with-names" -title="New IETF Database schema, without name tables" -modelviz.py --exclude="$proxy,$legacy,$names" --title "$title" $apps > $graph.dot && dot -Tpng $graph.dot > $graph.png - -echo "Generate new model without names and subevents" -graph="models" -title="New IETF Database schema, without name tables and subevents" -modelviz.py --exclude="$proxy,$legacy,$names,$events" --title "$title" $apps > $graph.dot && dot -Tpng $graph.dot > $graph.png diff --git a/bin/mm_hourly b/bin/mm_hourly deleted file mode 100755 index e371fd611b..0000000000 --- a/bin/mm_hourly +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Hourly datatracker jobs, ***run as mailman*** -# -# This script is expected to be triggered by cron from -# $DTDIR/etc/cron.d/datatracker which should be symlinked from -# /etc/cron.d/ - -export LANG=en_US.UTF-8 -export PYTHONIOENCODING=utf-8 - -# Make sure we stop if something goes wrong: -program=${0##*/} -trap 'echo "$program($LINENO): Command failed with error code $? ([$$] $0 $*)"; exit 1' ERR - -DTDIR=/a/www/ietf-datatracker/web -cd $DTDIR/ - -# Set up the virtual environment -source $DTDIR/env/bin/activate - -logger -p user.info -t cron "Running $DTDIR/bin/mm_hourly" - diff --git a/bin/monthly b/bin/monthly deleted file mode 100755 index 1d36abc210..0000000000 --- a/bin/monthly +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -# Weekly datatracker jobs. -# -# This script is expected to be triggered by cron from -# /etc/cron.d/datatracker -export LANG=en_US.UTF-8 -export PYTHONIOENCODING=utf-8 - -DTDIR=/a/www/ietf-datatracker/web -cd $DTDIR/ - -# Set up the virtual environment -source $DTDIR/env/bin/activate - -logger -p user.info -t cron "Running $DTDIR/bin/monthly" - diff --git a/bin/release-coverage b/bin/release-coverage deleted file mode 100755 index 22177c17a6..0000000000 --- a/bin/release-coverage +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -zcat release-coverage.json.gz | jq 'to_entries[] | [.value.time, .key, .value.code.coverage, .value.template.coverage, .value.url.coverage] ' 2>/dev/null | tr "\n][" " \n" | tr -d ' "Z' | tr ",T" " " | sort -n | cut -c 2- | sed -n '/2015-03-10/,$p' diff --git a/bin/update b/bin/update deleted file mode 100755 index bcb6e8b129..0000000000 --- a/bin/update +++ /dev/null @@ -1,229 +0,0 @@ -#!/bin/bash - -version="0.34" -program=$(basename $0) - -NEW="" # If there are more than $NEW % new lines, skip update -OLD="" # If there are more than $OLD % deleted lines, skip update -FILE="" -verbose="" -silent="" - -# ---------------------------------------------------------------------- -function usage() { -cat < -EOF -exit -} - - -# ---------------------------------------------------------------------- -function note() { - if [ -n "$verbose" ]; then - echo -e "$program: $*" - fi -} - -# ---------------------------------------------------------------------- -function warn() { - [ "$QUIET" ] || echo -e "$program: $*" -} - -# ---------------------------------------------------------------------- -function err() { - echo -e "$program: $*" > /dev/stderr -} - -# ----------------------------------------------------------------------------- -function leave() { - errcode=$1; shift - if [ "$errcode" -ge "2" ]; then warn "$*"; else note "$*"; fi - if [ -f "$tempfile" ]; then rm $tempfile; fi - if [ -f "$difffile" ]; then rm $difffile; fi - if [ "$errcode" = "1" -a "$RESULT" = "0" ]; then exit 0; else exit $errcode; fi -} - -# ---------------------------------------------------------------------- -# Set up error trap -trap 'leave 127 "$program($LINENO): Command failed with error code $? while processing '$origfile'."' ERR - -# exit with a message if a command fails -set -e - -# ---------------------------------------------------------------------- -# Get any options -# - -# Default values -PAT="\$path\$base.%Y-%m-%d_%H%M" -RESULT="0" -QUIET="" - -# Based on the sample code in /usr/share/doc/util-linux/examples/parse.bash.gz -if [ "$(uname)" = "Linux" ]; then - GETOPT_RESULT=$(getopt -o bc:ef:hn:o:p:qrvV --long backup,maxchg:,empty,file:,help,maxnew:,maxold:,prefix:,report,quiet,verbose,version -n "$program" -- "$@") -else - GETOPT_RESULT=$(getopt bc:ef:hn:o:p:qrvV "$@") -fi - -if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi - -note "GETOPT_RESULT: $GETOPT_RESULT" -eval set -- "$GETOPT_RESULT" - -while true ; do - case "$1" in - -b|--backup) backup=1; shift ;; # Back up earlier versions by creating a backup file - -c|--maxchg) CHG="$2"; shift 2 ;; # Limit on percentage of changed lines - -e|--empty) empty=1; shift ;; # Permit the update to be empty (default: discard) - -f|--file) FILE="$2"; shift 2 ;; # Read input from FILE instead of standard input - -h|--help) usage; shift ;; # Show this text and exit - -n|--maxnew) NEW="$2"; shift 2 ;; # Limit on percentage of new (added) lines - -o|--maxold) OLD="$2"; shift 2 ;; # Limit on percentage of old (deleted) lines - -p|--pat*) PAT="$2"; shift 2 ;; # Backup name base ('$path$base.%Y%m%d_%H%M') - -q|--quiet) QUIET=1; shift;; # Be less verbose - -r|--result) RESULT=1; shift ;; # Return 1 if update not done - -v|--verbose) verbose=1; shift ;; # Be more verbose about what's happening - -V|--version) echo -e "$program\t$version"; exit;; # Show version and exit - --) shift ; break ;; - *) echo "$program: Internal error, inconsistent option specification." ; exit 1 ;; - esac -done - -if [ $CHG ]; then OLD=$CHG; NEW=$CHG; fi - -if [ $# -lt 1 ]; then echo -e "$program: Missing output filename\n"; usage; fi - -origfile=$1 -tempfile=$(mktemp) -difffile=$(mktemp) - -if [ -e "$origfile" ]; then - cp -p $origfile $tempfile # For ownership and permissions - cat $FILE > $tempfile - [ "$FILE" ] && touch -r $FILE $tempfile - # This won't work if we don't have sufficient privileges: - #chown --reference=$origfile $tempfile - #chmod --reference=$origfile $tempfile -else - cat $FILE > $origfile - [ "$FILE" ] && touch -r $FILE $tempfile - leave 0 "Created file '$origfile'" -fi - -origlen=$(wc -c < $origfile) -newlen=$(wc -c < $tempfile) - -if [ $origlen = 0 -a $newlen = 0 ]; then - rm $tempfile - leave 1 "New content is identical (and void) - not updating '$origfile'." -fi -if [ $newlen = 0 -a -z "$empty" ]; then - leave 1 "New content is void - not updating '$origfile'." -fi - -diff $origfile $tempfile > $difffile || [ $? -le 1 ] && true # suppress the '1' error code on differences -difflen=$(wc -l < $difffile) -if [ $difflen = 0 ]; then - leave 1 "New content is identical - not updating '$origfile'." -fi - -if [ "$OLD" -o "$NEW" ]; then - - if [ "$NEW" ]; then maxnew=$(( $origlen * $NEW / 100 )); fi - if [ "$OLD" ]; then maxdel=$(( $origlen * $OLD / 100 )); fi - - newcount=$(grep "^> " $difffile | wc -c) - outcount=$(grep "^< " $difffile | wc -c) - delcount=$(grep "^! " $difffile | wc -c) - delcount=$(( $outcount + $delcount )) - rm $difffile - - if [ "$OLD" ]; then - if [ "$delcount" -ge "$maxdel" ]; then - cp $tempfile $origfile.update - leave 2 "New content has too many removed lines ($delcount/$origlen)\n - not updating '$origfile'.\nNew content placed in '$origfile.update' instead" - fi - fi - if [ "$NEW" ]; then - if [ "$newcount" -ge "$maxnew" ]; then - cp $tempfile $origfile.update - leave 2 "New content has too many added lines ($newcount/$origlen)\n - not updating '$origfile'.\nNew content placed in '$origfile.update' instead" - fi - fi -fi - -if [ "$backup" ]; then - - path=${origfile%/*} - name=${origfile##*/} - base=${name%.*} - ext=${origfile##*.} - - if [ "$ext" = "$origfile" ]; then - ext="" - elif [ ! "${ext%/*}" = "$ext" ]; then - ext="" - else - ext=".$ext" - fi - - if [ "$path" = "$origfile" ]; then - path="" - else - path="$path/" - fi - - ver=1 - backfile=$(eval date +"$PAT") - backpath="${backfile%/*}" - if [ "$backpath" = "$backfile" ]; then - backpath="." - fi - if [ ! -d $backpath ]; then - if [ -e $backpath ]; then - leave 3 "The backup path '$backpath' exists but isn't a directory" - else - mkdir -p $backpath - fi - fi - while [ -e "$backfile,$ver$ext" ]; do - ver=$(( $ver+1 )) - done - note "Saving backup: $backfile,$ver$ext" - cp -p "$origfile" "$backfile,$ver$ext" - chmod -w "$backfile,$ver$ext" || true -fi - -if ! mv $tempfile $origfile; then cp -p $tempfile $origfile; fi -leave 0 "Updated file '$origfile'" diff --git a/bin/weekly b/bin/weekly deleted file mode 100755 index 8e01c273ca..0000000000 --- a/bin/weekly +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# Weekly datatracker jobs. -# -# This script is expected to be triggered by cron from -# /etc/cron.d/datatracker -export LANG=en_US.UTF-8 -export PYTHONIOENCODING=utf-8 - -DTDIR=/a/www/ietf-datatracker/web -cd $DTDIR/ - -# Set up the virtual environment -source $DTDIR/env/bin/activate - -logger -p user.info -t cron "Running $DTDIR/bin/weekly" - - -# Send out weekly summaries of apikey usage - -$DTDIR/ietf/manage.py send_apikey_usage_emails - diff --git a/client/agenda/Agenda.vue b/client/agenda/Agenda.vue index 0212fc8cb6..34a3751f23 100644 --- a/client/agenda/Agenda.vue +++ b/client/agenda/Agenda.vue @@ -58,14 +58,17 @@ n-button( :type='agendaStore.isTimezoneMeeting ? `primary` : `default`' @click='setTimezone(`meeting`)' + :text-color='agendaStore.isTimezoneMeeting ? `#FFF` : null' ) Meeting n-button( :type='agendaStore.isTimezoneLocal ? `primary` : `default`' @click='setTimezone(`local`)' + :text-color='agendaStore.isTimezoneLocal ? `#FFF` : null' ) Local n-button( :type='agendaStore.timezone === `UTC` ? `primary` : `default`' @click='setTimezone(`UTC`)' + :text-color='agendaStore.timezone === `UTC` ? `#FFF` : null' ) UTC n-select.agenda-timezone-ddn( v-if='siteStore.viewport > 1250' diff --git a/client/agenda/AgendaQuickAccess.vue b/client/agenda/AgendaQuickAccess.vue index 5952e6de1c..99adc1027e 100644 --- a/client/agenda/AgendaQuickAccess.vue +++ b/client/agenda/AgendaQuickAccess.vue @@ -62,7 +62,8 @@ n-button.mt-2( id='agenda-quickaccess-calview-btn' block - color='#6c757d' + color='#6f42c1' + text-color='#FFF' size='large' strong @click='agendaStore.$patch({ calendarShown: true })' @@ -78,8 +79,8 @@ n-button.mt-2( id='agenda-quickaccess-addtocal-btn' block - secondary - color='#6c757d' + :color='siteStore.theme === `dark` ? `rgba(111, 66, 193, .3)` : `#e2d9f3`' + :text-color='siteStore.theme === `dark` ? `#e2d9f3` : `#59359a`' size='large' strong ) diff --git a/client/agenda/AgendaScheduleCalendar.vue b/client/agenda/AgendaScheduleCalendar.vue index d8a30ca4ad..9b56b7f5a7 100644 --- a/client/agenda/AgendaScheduleCalendar.vue +++ b/client/agenda/AgendaScheduleCalendar.vue @@ -11,14 +11,17 @@ n-drawer(v-model:show='isShown', placement='bottom', :height='state.drawerHeight n-button( :type='agendaStore.isTimezoneMeeting ? `primary` : `default`' @click='setTimezone(`meeting`)' + :text-color='agendaStore.isTimezoneMeeting ? `#FFF` : null' ) Meeting n-button( :type='agendaStore.isTimezoneLocal ? `primary` : `default`' @click='setTimezone(`local`)' + :text-color='agendaStore.isTimezoneLocal ? `#FFF` : null' ) Local n-button( :type='agendaStore.timezone === `UTC` ? `primary` : `default`' @click='setTimezone(`UTC`)' + :text-color='agendaStore.timezone === `UTC` ? `#FFF` : null' ) UTC n-divider(vertical) n-button.me-2( @@ -32,7 +35,7 @@ n-drawer(v-model:show='isShown', placement='bottom', :height='state.drawerHeight n-badge.ms-2(:value='agendaStore.selectedCatSubs.length', processing) n-button( ghost - color='gray' + :color='siteStore.theme === `dark` ? `#e35d6a` : `gray`' strong @click='close' ) diff --git a/client/components/Polls.vue b/client/components/Polls.vue index f5023995a3..30cc9e8f36 100644 --- a/client/components/Polls.vue +++ b/client/components/Polls.vue @@ -3,9 +3,11 @@ n-data-table( v-if='state.items.length > 0' :data='state.items' - :columns='columns' + :columns='state.columns' striped ) + span.text-danger(v-else-if='state.errMessage') + em {{ state.errMessage }} span.text-body-secondary(v-else) em No polls available. @@ -13,9 +15,8 @@ diff --git a/client/shared/xslugify.js b/client/shared/xslugify.js index daf0bdf2ba..e1f7a34483 100644 --- a/client/shared/xslugify.js +++ b/client/shared/xslugify.js @@ -1,5 +1,5 @@ import slugify from 'slugify' export default (str) => { - return slugify(str.replace('/', '-'), { lower: true }) + return slugify(str.replaceAll('/', '-'), { lower: true }) } diff --git a/debug.py b/debug.py index bf34367cce..4f0d64bae2 100644 --- a/debug.py +++ b/debug.py @@ -3,15 +3,7 @@ import sys import time as timeutils import inspect -from typing import Callable -try: - import syslog - logger = syslog.syslog # type: Callable -except ImportError: # import syslog will fail on Windows boxes - import logging - logging.basicConfig(filename='tracker.log',level=logging.INFO) - logger = logging.info try: from pprint import pformat @@ -55,7 +47,7 @@ def fix(s,n=64): if len(s) > n+3: s = s[:n]+"..." return s - def wrap(fn, *params,**kwargs): + def wrap(*params,**kwargs): call = wrap.callcount = wrap.callcount + 1 indent = ' ' * _report_indent[0] @@ -81,8 +73,8 @@ def wrap(fn, *params,**kwargs): return ret wrap.callcount = 0 if debug: - from decorator import decorator - return decorator(wrap, fn) + from functools import update_wrapper + return update_wrapper(wrap, fn) else: return fn @@ -119,7 +111,7 @@ def clock(s): def time(fn): """Decorator to print timing information about a function call. """ - def wrap(fn, *params,**kwargs): + def wrap(*params,**kwargs): indent = ' ' * _report_indent[0] fc = "%s.%s()" % (fn.__module__, fn.__name__,) @@ -132,8 +124,8 @@ def wrap(fn, *params,**kwargs): return ret wrap.callcount = 0 if debug: - from decorator import decorator - return decorator(wrap, fn) + from functools import update_wrapper + return update_wrapper(wrap, fn) else: return fn @@ -155,13 +147,6 @@ def showpos(name): indent = ' ' * (_report_indent[0]) sys.stderr.write("%s%s:%s: %s: '%s'\n" % (indent, fn, line, name, value)) -def log(name): - if debug: - frame = inspect.stack()[1][0] - value = eval(name, frame.f_globals, frame.f_locals) - indent = ' ' * (_report_indent[0]) - logger("%s%s: %s" % (indent, name, value)) - def pprint(name): if debug: frame = inspect.stack()[1][0] @@ -190,7 +175,7 @@ def type(name): value = eval(name, frame.f_globals, frame.f_locals) indent = ' ' * (_report_indent[0]) sys.stderr.write("%s%s: %s\n" % (indent, name, value)) - + def say(s): if debug: indent = ' ' * (_report_indent[0]) @@ -205,11 +190,11 @@ def wrapper(*args, **kwargs): prof.dump_stats(datafn) return retval if debug: - from decorator import decorator - return decorator(wrapper, fn) + from functools import update_wrapper + return update_wrapper(wrapper, fn) else: return fn - + def traceback(levels=None): if debug: indent = ' ' * (_report_indent[0]) diff --git a/dev/build/Dockerfile b/dev/build/Dockerfile index 2ffce35495..e7cc510bc7 100644 --- a/dev/build/Dockerfile +++ b/dev/build/Dockerfile @@ -1,17 +1,42 @@ -FROM ghcr.io/ietf-tools/datatracker-app-base:latest -LABEL maintainer="IETF Tools Team " - -ENV DEBIAN_FRONTEND=noninteractive - -COPY . . -COPY ./dev/build/start.sh ./start.sh -RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt -RUN chmod +x start.sh && \ - chmod +x docker/scripts/app-create-dirs.sh && \ - sh ./docker/scripts/app-create-dirs.sh - -VOLUME [ "/assets" ] - -EXPOSE 8000 - -CMD ["./start.sh"] \ No newline at end of file +FROM ghcr.io/ietf-tools/datatracker-app-base:latest +LABEL maintainer="IETF Tools Team " + +ENV DEBIAN_FRONTEND=noninteractive + +# uid 498 = wwwrun and gid 496 = www on ietfa +RUN groupadd -g 1000 datatracker && \ + useradd -c "Datatracker User" -u 1000 -g datatracker -m -s /bin/false datatracker + +RUN apt-get purge -y imagemagick imagemagick-6-common + +# Install libreoffice (needed via PPT2PDF_COMMAND) +RUN echo "deb http://deb.debian.org/debian bullseye-backports main" > /etc/apt/sources.list.d/bullseye-backports.list && \ + apt-get update && \ + apt-get -qyt bullseye-backports install libreoffice-nogui + +COPY . . +COPY ./dev/build/start.sh ./start.sh +COPY ./dev/build/datatracker-start.sh ./datatracker-start.sh +COPY ./dev/build/migration-start.sh ./migration-start.sh +COPY ./dev/build/celery-start.sh ./celery-start.sh +COPY ./dev/build/gunicorn.conf.py ./gunicorn.conf.py + +RUN pip3 --disable-pip-version-check --no-cache-dir install -r requirements.txt && \ + echo '# empty' > ietf/settings_local.py && \ + ietf/manage.py patch_libraries && \ + rm -f ietf/settings_local.py + +RUN chmod +x start.sh && \ + chmod +x datatracker-start.sh && \ + chmod +x migration-start.sh && \ + chmod +x celery-start.sh && \ + chmod +x docker/scripts/app-create-dirs.sh && \ + sh ./docker/scripts/app-create-dirs.sh + +RUN mkdir -p /a + +VOLUME [ "/a" ] + +EXPOSE 8000 + +CMD ["./start.sh"] diff --git a/dev/build/celery-start.sh b/dev/build/celery-start.sh new file mode 100644 index 0000000000..f6284e84db --- /dev/null +++ b/dev/build/celery-start.sh @@ -0,0 +1,33 @@ +#!/bin/bash -e +# +# Run a celery worker +# +echo "Running Datatracker checks..." +./ietf/manage.py check + +if ! ietf/manage.py migrate --check ; then + echo "Unapplied migrations found, waiting to start..." + sleep 5 + while ! ietf/manage.py migrate --check ; do + echo "... still waiting for migrations..." + sleep 5 + done +fi + +echo "Starting Celery..." + +cleanup () { + # Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit. + if [[ -n "${celery_pid}" ]]; then + echo "Gracefully terminating celery worker. This may take a few minutes if tasks are in progress..." + kill -TERM "${celery_pid}" + wait "${celery_pid}" + fi +} + +trap 'trap "" TERM; cleanup' TERM + +# start celery in the background so we can trap the TERM signal +celery "$@" & +celery_pid=$! +wait "${celery_pid}" diff --git a/dev/build/datatracker-start.sh b/dev/build/datatracker-start.sh new file mode 100644 index 0000000000..5eb99c4745 --- /dev/null +++ b/dev/build/datatracker-start.sh @@ -0,0 +1,41 @@ +#!/bin/bash -e + +echo "Running Datatracker checks..." +./ietf/manage.py check + +if ! ietf/manage.py migrate --check ; then + echo "Unapplied migrations found, waiting to start..." + sleep 5 + while ! ietf/manage.py migrate --check ; do + echo "... still waiting for migrations..." + sleep 5 + done +fi + +echo "Starting Datatracker..." + +# trap TERM and shut down gunicorn +cleanup () { + if [[ -n "${gunicorn_pid}" ]]; then + echo "Terminating gunicorn..." + kill -TERM "${gunicorn_pid}" + wait "${gunicorn_pid}" + fi +} + +trap 'trap "" TERM; cleanup' TERM + +# start gunicorn in the background so we can trap the TERM signal +gunicorn \ + -c /workspace/gunicorn.conf.py \ + --workers "${DATATRACKER_GUNICORN_WORKERS:-9}" \ + --max-requests "${DATATRACKER_GUNICORN_MAX_REQUESTS:-32768}" \ + --timeout "${DATATRACKER_GUNICORN_TIMEOUT:-180}" \ + --bind :8000 \ + --log-level "${DATATRACKER_GUNICORN_LOG_LEVEL:-info}" \ + --capture-output \ + --access-logfile -\ + ${DATATRACKER_GUNICORN_EXTRA_ARGS} \ + ietf.wsgi:application & +gunicorn_pid=$! +wait "${gunicorn_pid}" diff --git a/dev/build/gunicorn.conf.py b/dev/build/gunicorn.conf.py new file mode 100644 index 0000000000..48661ef77d --- /dev/null +++ b/dev/build/gunicorn.conf.py @@ -0,0 +1,49 @@ +# Copyright The IETF Trust 2024, All Rights Reserved + +# Log as JSON on stdout (to distinguish from Django's logs on stderr) +# +# This is applied as an update to gunicorn's glogging.CONFIG_DEFAULTS. +logconfig_dict = { + "version": 1, + "disable_existing_loggers": False, + "root": {"level": "INFO", "handlers": ["console"]}, + "loggers": { + "gunicorn.error": { + "level": "INFO", + "handlers": ["console"], + "propagate": False, + "qualname": "gunicorn.error" + }, + + "gunicorn.access": { + "level": "INFO", + "handlers": ["access_console"], + "propagate": False, + "qualname": "gunicorn.access" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "json", + "stream": "ext://sys.stdout" + }, + "access_console": { + "class": "logging.StreamHandler", + "formatter": "access_json", + "stream": "ext://sys.stdout" + }, + }, + "formatters": { + "json": { + "class": "ietf.utils.jsonlogger.DatatrackerJsonFormatter", + "style": "{", + "format": "{asctime}{levelname}{message}{name}{process}", + }, + "access_json": { + "class": "ietf.utils.jsonlogger.GunicornRequestJsonFormatter", + "style": "{", + "format": "{asctime}{levelname}{message}{name}{process}", + } + } +} diff --git a/dev/build/migration-start.sh b/dev/build/migration-start.sh new file mode 100644 index 0000000000..211638aec6 --- /dev/null +++ b/dev/build/migration-start.sh @@ -0,0 +1,6 @@ +#!/bin/bash -e + +echo "Running Datatracker migrations..." +./ietf/manage.py migrate --settings=settings_local + +echo "Done!" diff --git a/dev/build/start.sh b/dev/build/start.sh index ef64ca7b30..ef6c7fc0d9 100644 --- a/dev/build/start.sh +++ b/dev/build/start.sh @@ -1,10 +1,26 @@ #!/bin/bash - -echo "Running Datatracker checks..." -./ietf/manage.py check - -echo "Running Datatracker migrations..." -./ietf/manage.py migrate --settings=settings_local - -echo "Starting Datatracker..." -./ietf/manage.py runserver 0.0.0.0:8000 --settings=settings_local +# +# Environment config: +# +# CONTAINER_ROLE - datatracker, celery, or beat (defaults to datatracker) +# +case "${CONTAINER_ROLE:-datatracker}" in + auth) + exec ./datatracker-start.sh + ;; + beat) + exec ./celery-start.sh --app=ietf beat + ;; + celery) + exec ./celery-start.sh --app=ietf worker + ;; + datatracker) + exec ./datatracker-start.sh + ;; + migrations) + exec ./migration-start.sh + ;; + *) + echo "Unknown role '${CONTAINER_ROLE}'" + exit 255 +esac diff --git a/dev/coverage-action/action.yml b/dev/coverage-action/action.yml index b8d732a534..60c8de2d92 100644 --- a/dev/coverage-action/action.yml +++ b/dev/coverage-action/action.yml @@ -35,7 +35,7 @@ outputs: changelog: description: Changelog with headers prepended and coverage stats + chart appended runs: - using: 'node16' + using: 'node20' main: 'index.js' branding: icon: layers diff --git a/dev/coverage-action/index.js b/dev/coverage-action/index.js index 57249bfdb1..5a1c690be3 100644 --- a/dev/coverage-action/index.js +++ b/dev/coverage-action/index.js @@ -5,20 +5,20 @@ const find = require('lodash/find') const round = require('lodash/round') const fs = require('fs/promises') const { DateTime } = require('luxon') -const isPlainObject = require('lodash/isPlainObject') +// const isPlainObject = require('lodash/isPlainObject') const dec = new TextDecoder() async function main () { const token = core.getInput('token') - const tokenCommon = core.getInput('tokenCommon') + // const tokenCommon = core.getInput('tokenCommon') const inputCovPath = core.getInput('coverageResultsPath') // 'data/coverage-raw.json' const outputCovPath = core.getInput('coverageResultsPath') // 'data/coverage.json' const outputHistPath = core.getInput('histCoveragePath') // 'data/historical-coverage.json' const relVersionRaw = core.getInput('version') // 'v7.47.0' const relVersion = relVersionRaw.indexOf('v') === 0 ? relVersionRaw.substring(1) : relVersionRaw const gh = github.getOctokit(token) - const ghCommon = github.getOctokit(tokenCommon) + // const ghCommon = github.getOctokit(tokenCommon) const owner = github.context.repo.owner // 'ietf-tools' const repo = github.context.repo.repo // 'datatracker' const sender = github.context.payload.sender.login // 'rjsparks' @@ -116,137 +116,137 @@ async function main () { } // -> Coverage Chart - if (chartsDirListing.some(c => c.name === `${newRelease.id}.svg`)) { - console.info(`Chart SVG already exists for ${newRelease.name}, skipping...`) - } else { - console.info(`Generating chart SVG for ${newRelease.name}...`) + // if (chartsDirListing.some(c => c.name === `${newRelease.id}.svg`)) { + // console.info(`Chart SVG already exists for ${newRelease.name}, skipping...`) + // } else { + // console.info(`Generating chart SVG for ${newRelease.name}...`) - const { ChartJSNodeCanvas } = require('chartjs-node-canvas') - const chartJSNodeCanvas = new ChartJSNodeCanvas({ type: 'svg', width: 850, height: 300, backgroundColour: '#FFFFFF' }) + // const { ChartJSNodeCanvas } = require('chartjs-node-canvas') + // const chartJSNodeCanvas = new ChartJSNodeCanvas({ type: 'svg', width: 850, height: 300, backgroundColour: '#FFFFFF' }) - // -> Reorder versions - const versions = [] - for (const [key, value] of Object.entries(covData)) { - if (isPlainObject(value)) { - const vRel = find(releases, r => r.tag_name === key || r.tag_name === `v${key}`) - if (!vRel) { - continue - } - versions.push({ - tag: key, - time: vRel.created_at, - stats: { - code: round(value.code * 100, 2), - template: round(value.template * 100, 2), - url: round(value.url * 100, 2) - } - }) - } - } - const roVersions = orderBy(versions, ['time', 'tag'], ['asc', 'asc']) + // // -> Reorder versions + // const versions = [] + // for (const [key, value] of Object.entries(covData)) { + // if (isPlainObject(value)) { + // const vRel = find(releases, r => r.tag_name === key || r.tag_name === `v${key}`) + // if (!vRel) { + // continue + // } + // versions.push({ + // tag: key, + // time: vRel.created_at, + // stats: { + // code: round(value.code * 100, 2), + // template: round(value.template * 100, 2), + // url: round(value.url * 100, 2) + // } + // }) + // } + // } + // const roVersions = orderBy(versions, ['time', 'tag'], ['asc', 'asc']) - // -> Fill axis + data points - const labels = [] - const datasetCode = [] - const datasetTemplate = [] - const datasetUrl = [] + // // -> Fill axis + data points + // const labels = [] + // const datasetCode = [] + // const datasetTemplate = [] + // const datasetUrl = [] - for (const ver of roVersions) { - labels.push(ver.tag) - datasetCode.push(ver.stats.code) - datasetTemplate.push(ver.stats.template) - datasetUrl.push(ver.stats.url) - } + // for (const ver of roVersions) { + // labels.push(ver.tag) + // datasetCode.push(ver.stats.code) + // datasetTemplate.push(ver.stats.template) + // datasetUrl.push(ver.stats.url) + // } - // -> Generate chart SVG - const outputStream = chartJSNodeCanvas.renderToBufferSync({ - type: 'line', - options: { - borderColor: '#CCC', - layout: { - padding: 20 - }, - plugins: { - legend: { - position: 'bottom', - labels: { - font: { - size: 11 - } - } - } - }, - scales: { - x: { - ticks: { - font: { - size: 10 - } - } - }, - y: { - ticks: { - callback: (value) => { - return `${value}%` - }, - font: { - size: 10 - } - } - } - } - }, - data: { - labels, - datasets: [ - { - label: 'Code', - data: datasetCode, - borderWidth: 2, - borderColor: '#E53935', - backgroundColor: '#C6282833', - fill: false, - cubicInterpolationMode: 'monotone', - tension: 0.4, - pointRadius: 0 - }, - { - label: 'Templates', - data: datasetTemplate, - borderWidth: 2, - borderColor: '#039BE5', - backgroundColor: '#0277BD33', - fill: false, - cubicInterpolationMode: 'monotone', - tension: 0.4, - pointRadius: 0 - }, - { - label: 'URLs', - data: datasetUrl, - borderWidth: 2, - borderColor: '#7CB342', - backgroundColor: '#558B2F33', - fill: false, - cubicInterpolationMode: 'monotone', - tension: 0.4, - pointRadius: 0 - } - ] - } - }, 'image/svg+xml') - const svg = Buffer.from(outputStream).toString('base64') + // // -> Generate chart SVG + // const outputStream = chartJSNodeCanvas.renderToBufferSync({ + // type: 'line', + // options: { + // borderColor: '#CCC', + // layout: { + // padding: 20 + // }, + // plugins: { + // legend: { + // position: 'bottom', + // labels: { + // font: { + // size: 11 + // } + // } + // } + // }, + // scales: { + // x: { + // ticks: { + // font: { + // size: 10 + // } + // } + // }, + // y: { + // ticks: { + // callback: (value) => { + // return `${value}%` + // }, + // font: { + // size: 10 + // } + // } + // } + // } + // }, + // data: { + // labels, + // datasets: [ + // { + // label: 'Code', + // data: datasetCode, + // borderWidth: 2, + // borderColor: '#E53935', + // backgroundColor: '#C6282833', + // fill: false, + // cubicInterpolationMode: 'monotone', + // tension: 0.4, + // pointRadius: 0 + // }, + // { + // label: 'Templates', + // data: datasetTemplate, + // borderWidth: 2, + // borderColor: '#039BE5', + // backgroundColor: '#0277BD33', + // fill: false, + // cubicInterpolationMode: 'monotone', + // tension: 0.4, + // pointRadius: 0 + // }, + // { + // label: 'URLs', + // data: datasetUrl, + // borderWidth: 2, + // borderColor: '#7CB342', + // backgroundColor: '#558B2F33', + // fill: false, + // cubicInterpolationMode: 'monotone', + // tension: 0.4, + // pointRadius: 0 + // } + // ] + // } + // }, 'image/svg+xml') + // const svg = Buffer.from(outputStream).toString('base64') - // -> Upload to common repo - console.info(`Uploading chart SVG for ${newRelease.name}...`) - await ghCommon.rest.repos.createOrUpdateFileContents({ - owner, - repo: repoCommon, - path: `assets/graphs/datatracker/${newRelease.id}.svg`, - message: `chore: update datatracker release chart for release ${newRelease.name}`, - content: svg - }) - } + // // -> Upload to common repo + // console.info(`Uploading chart SVG for ${newRelease.name}...`) + // await ghCommon.rest.repos.createOrUpdateFileContents({ + // owner, + // repo: repoCommon, + // path: `assets/graphs/datatracker/${newRelease.id}.svg`, + // message: `chore: update datatracker release chart for release ${newRelease.name}`, + // content: svg + // }) + // } // -> Add to changelog body let formattedBody = '' @@ -265,7 +265,7 @@ async function main () { formattedBody += `![](https://img.shields.io/badge/Code-${covInfo.code}%25-${getCoverageColor(covInfo.code)}?style=flat-square)` formattedBody += `![](https://img.shields.io/badge/Templates-${covInfo.template}%25-${getCoverageColor(covInfo.template)}?style=flat-square)` formattedBody += `![](https://img.shields.io/badge/URLs-${covInfo.url}%25-${getCoverageColor(covInfo.url)}?style=flat-square)\n\n` - formattedBody += `![chart](https://raw.githubusercontent.com/${owner}/${repoCommon}/main/assets/graphs/datatracker/${newRelease.id}.svg)` + // formattedBody += `![chart](https://raw.githubusercontent.com/${owner}/${repoCommon}/main/assets/graphs/datatracker/${newRelease.id}.svg)` core.setOutput('changelog', formattedBody) } diff --git a/dev/coverage-action/package-lock.json b/dev/coverage-action/package-lock.json index 835f1a6cc8..8c9b97e026 100644 --- a/dev/coverage-action/package-lock.json +++ b/dev/coverage-action/package-lock.json @@ -11,27 +11,8 @@ "dependencies": { "@actions/core": "1.10.1", "@actions/github": "6.0.0", - "chart.js": "3.9.1", - "chartjs-node-canvas": "4.1.6", "lodash": "4.17.21", "luxon": "3.4.4" - }, - "devDependencies": { - "eslint": "8.57.0", - "eslint-config-standard": "17.1.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "6.1.1", - "npm-check-updates": "16.14.15" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" } }, "node_modules/@actions/core": { @@ -63,72 +44,6 @@ "undici": "^5.25.4" } }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "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==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/@fastify/busboy": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", @@ -137,298 +52,6 @@ "node": ">=14" } }, - "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==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "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==", - "dev": true - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", - "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==", - "dependencies": { - "detect-libc": "^1.0.3", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.5", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "bin": { - "node-pre-gyp": "bin/node-pre-gyp" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", - "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", - "dev": true, - "dependencies": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "npm-pick-manifest": "^8.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/installed-package-contents": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", - "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", - "dev": true, - "dependencies": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "bin": { - "installed-package-contents": "lib/index.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", - "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", - "dev": true, - "dependencies": { - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", - "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", - "dev": true, - "dependencies": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/promise-spawn": "^6.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^3.0.0", - "which": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/run-script/node_modules/which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -549,9889 +172,239 @@ "@octokit/openapi-types": "^19.0.0" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true, - "engines": { - "node": ">=14" - } + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.0.0.tgz", - "integrity": "sha512-ZVPVDi1E8oeXlYqkGRtX0CkzLTwE2zt62bjWaWKaAvI8NZqHzlMvGeSNDpW+JB3+aKanYb4UETJOF1/CxGPemA==", - "dev": true, - "engines": { - "node": ">=12.22.0" - } + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dev": true, - "dependencies": { - "graceful-fs": "4.2.10" - }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "engines": { - "node": ">=12.22.0" + "node": ">=0.10.0" } }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/@pnpm/npm-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.1.0.tgz", - "integrity": "sha512-Oe6ntvgsMTE3hDIqy6sajqHF+MnzJrOF06qC2QSiUEybLL7cp6tjoKUa32gpd9+KPVl4QyMs3E3nsXrx/Vdnlw==", - "dev": true, - "dependencies": { - "@pnpm/config.env-replace": "^1.0.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } }, - "node_modules/@sigstore/bundle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.0.0.tgz", - "integrity": "sha512-yLvrWDOh6uMOUlFCTJIZEnwOT9Xte7NPXUqVexEKGSF5XtBAuSg5du0kn3dRR0p47a4ah10Y0mNt8+uyeQXrBQ==", - "dev": true, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "wrappy": "1" } }, - "node_modules/@sigstore/protobuf-specs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.0.tgz", - "integrity": "sha512-8ZhZKAVfXjIspDWwm3D3Kvj0ddbJ0HqDZ/pOs5cx88HpT8mVsotFrg7H1UMnXOuDHz6Zykwxn4mxG3QLuN+RUg==", - "dev": true, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, - "node_modules/@sigstore/tuf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.3.tgz", - "integrity": "sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==", - "dev": true, + "node_modules/undici": { + "version": "5.26.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", + "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", "dependencies": { - "@sigstore/protobuf-specs": "^0.2.0", - "tuf-js": "^1.1.7" + "@fastify/busboy": "^2.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=14.0" } }, - "node_modules/@sindresorhus/is": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", - "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + }, + "dependencies": { + "@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "requires": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" } }, - "node_modules/@tufjs/canonical-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", - "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "requires": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" } }, - "node_modules/@tufjs/models": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", - "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", - "dev": true, - "dependencies": { - "@tufjs/canonical-json": "1.0.0", - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "@actions/http-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", + "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "requires": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" } }, - "node_modules/@tufjs/models/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" - } + "@fastify/busboy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", + "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" }, - "node_modules/@tufjs/models/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" - } + "@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "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/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "node_modules/are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "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", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" - }, - "node_modules/boxen": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.2.tgz", - "integrity": "sha512-1Z4UJabXUP1/R9rLpoU3O2lEMnG3pPLAs/ZD2lF3t2q7qD5lM8rqbtnvtvm4N0wEyNlE+9yZVTVAGmd1V5jabg==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/boxen/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/boxen/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, - "node_modules/cacache": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.3.tgz", - "integrity": "sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==", - "dev": true, - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/cacache/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/cacache/node_modules/fs-minipass": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", - "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", - "dev": true, - "dependencies": { - "minipass": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", - "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/cacache/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/cacache/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.8.tgz", - "integrity": "sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==", - "dev": true, - "dependencies": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.2", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/canvas": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz", - "integrity": "sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", - "simple-get": "^3.0.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" - }, - "node_modules/chartjs-node-canvas": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-4.1.6.tgz", - "integrity": "sha512-UQJbPWrvqB/FoLclGA9BaLQmZbzSYlujF4w8NZd6Xzb+sqgACBb2owDX6m7ifCXLjUW5Nz0Qx0qqrTtQkkSoYw==", - "dependencies": { - "canvas": "^2.8.0", - "tslib": "^2.3.1" - }, - "peerDependencies": { - "chart.js": "^3.5.1" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/configstore": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", - "dev": true, - "dependencies": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/yeoman/configstore?sponsor=1" - } - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "dependencies": { - "mimic-response": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "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", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "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", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": 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", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^8.0.1", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", - "eslint-plugin-promise": "^6.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/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==", - "dev": true, - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "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==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "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", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-n": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz", - "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==", - "dev": true, - "peer": true, - "dependencies": { - "builtins": "^5.0.1", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.11.0", - "minimatch": "^3.1.2", - "resolve": "^1.22.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-n/node_modules/eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "peer": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-n/node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "peer": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-n/node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-n/node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "peer": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-plugin-n/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/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, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "dev": true, - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/fp-and-or": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.4.tgz", - "integrity": "sha512-+yRYRhpnFPWXSly/6V4Lw9IfOV26uu30kynGJ03PW+MnjOEQe45RZ141QcS0aJehYBYA50GfCDnsRbFJdhssRw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/global-dirs/node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.0.tgz", - "integrity": "sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/got/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2" - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", - "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/hosted-git-info/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", - "dev": true, - "dependencies": { - "minimatch": "^9.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ignore-walk/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/ignore-walk/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/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "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==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "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" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "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==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.11" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "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==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/json-parse-helpfulerror": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", - "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", - "dev": true, - "dependencies": { - "jju": "^1.1.0" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsonlines": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", - "integrity": "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==", - "dev": true - }, - "node_modules/jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true, - "engines": [ - "node >= 0.2.0" - ] - }, - "node_modules/keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "dev": true, - "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "dependencies": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/make-fetch-happen/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/make-fetch-happen/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", - "engines": { - "node": ">=8" - }, - "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", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "node_modules/minipass": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", - "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", - "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", - "dev": true, - "dependencies": { - "minipass": "^5.0.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-fetch/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "dependencies": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz", - "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^11.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^12.13 || ^14.13 || >=16" - } - }, - "node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "dev": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "dev": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "dependencies": { - "abbrev": "^1.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/node-gyp/node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "dev": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/normalize-package-data/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", - "dev": true, - "dependencies": { - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-check-updates": { - "version": "16.14.15", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", - "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", - "dev": true, - "dependencies": { - "chalk": "^5.3.0", - "cli-table3": "^0.6.3", - "commander": "^10.0.1", - "fast-memoize": "^2.5.2", - "find-up": "5.0.0", - "fp-and-or": "^0.1.4", - "get-stdin": "^8.0.0", - "globby": "^11.0.4", - "hosted-git-info": "^5.1.0", - "ini": "^4.1.1", - "js-yaml": "^4.1.0", - "json-parse-helpfulerror": "^1.0.3", - "jsonlines": "^0.1.1", - "lodash": "^4.17.21", - "make-fetch-happen": "^11.1.1", - "minimatch": "^9.0.3", - "p-map": "^4.0.0", - "pacote": "15.2.0", - "parse-github-url": "^1.0.2", - "progress": "^2.0.3", - "prompts-ncu": "^3.0.0", - "rc-config-loader": "^4.1.3", - "remote-git-tags": "^3.0.0", - "rimraf": "^5.0.5", - "semver": "^7.5.4", - "semver-utils": "^1.1.4", - "source-map-support": "^0.5.21", - "spawn-please": "^2.0.2", - "strip-ansi": "^7.1.0", - "strip-json-comments": "^5.0.1", - "untildify": "^4.0.0", - "update-notifier": "^6.0.2" - }, - "bin": { - "ncu": "build/src/bin/cli.js", - "npm-check-updates": "build/src/bin/cli.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/npm-check-updates/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm-check-updates/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/npm-check-updates/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm-check-updates/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm-check-updates/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/npm-check-updates/node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm-check-updates/node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm-check-updates/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm-check-updates/node_modules/strip-json-comments": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", - "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-install-checks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", - "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", - "dev": true, - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", - "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", - "dev": true, - "dependencies": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "dependencies": { - "lru-cache": "^7.5.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/npm-packlist": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", - "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", - "dev": true, - "dependencies": { - "ignore-walk": "^6.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", - "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", - "dev": true, - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", - "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", - "dev": true, - "dependencies": { - "make-fetch-happen": "^11.0.0", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^10.0.0", - "proc-log": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-registry-fetch/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "dependencies": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "dev": true, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.0.tgz", - "integrity": "sha512-hySwcV8RAWeAfPsXb9/HGSPn8lwDnv6fabH+obUZKX169QknRkRhPxd1yMubpKDskLFATkl3jHpNtVtDPFA0Wg==", - "dev": true, - "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pacote": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", - "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", - "dev": true, - "dependencies": { - "@npmcli/git": "^4.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^6.0.1", - "@npmcli/run-script": "^6.0.0", - "cacache": "^17.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^5.0.0", - "npm-package-arg": "^10.0.0", - "npm-packlist": "^7.0.0", - "npm-pick-manifest": "^8.0.0", - "npm-registry-fetch": "^14.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^1.3.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "bin": { - "pacote": "lib/bin.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/pacote/node_modules/fs-minipass": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", - "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", - "dev": true, - "dependencies": { - "minipass": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/pacote/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "dev": true, - "bin": { - "parse-github-url": "cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", - "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", - "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prompts-ncu": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prompts-ncu/-/prompts-ncu-3.0.0.tgz", - "integrity": "sha512-qyz9UxZ5MlPKWVhWrCmSZ1ahm2GVYdjLb8og2sg0IPth1KRuhcggHGuijz0e41dkx35p1t1q3GRISGH7QGALFA==", - "dev": true, - "dependencies": { - "kleur": "^4.0.1", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", - "dev": true, - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc-config-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", - "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", - "dev": true, - "dependencies": { - "debug": "^4.3.4", - "js-yaml": "^4.1.0", - "json5": "^2.2.2", - "require-from-string": "^2.0.2" - } - }, - "node_modules/rc-config-loader/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-package-json": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", - "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", - "dev": true, - "dependencies": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json/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/read-package-json/node_modules/glob": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", - "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/read-package-json/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/read-package-json/node_modules/minipass": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", - "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", - "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", - "dev": true, - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/registry-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", - "dev": true, - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/remote-git-tags": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remote-git-tags/-/remote-git-tags-3.0.0.tgz", - "integrity": "sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true - }, - "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==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "dev": true, - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semver-utils": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/semver-utils/-/semver-utils-1.1.4.tgz", - "integrity": "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==", - "dev": true - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - }, - "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==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/sigstore": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.8.0.tgz", - "integrity": "sha512-ogU8qtQ3VFBawRJ8wjsBEX/vIFeHuGs1fm4jZtjWQwjo8pfAt7T/rh+udlAN4+QUe0IzA8qRSc/YZ7dHP6kh+w==", - "dev": true, - "dependencies": { - "@sigstore/bundle": "^1.0.0", - "@sigstore/protobuf-specs": "^0.2.0", - "@sigstore/tuf": "^1.0.3", - "make-fetch-happen": "^11.0.1" - }, - "bin": { - "sigstore": "bin/sigstore.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "dependencies": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dev": true, - "dependencies": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.13.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spawn-please": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/spawn-please/-/spawn-please-2.0.2.tgz", - "integrity": "sha512-KM8coezO6ISQ89c1BzyWNtcn2V2kAVtwIXd3cN/V5a0xPYc1F/vydrRc01wsKFEQ/p+V1a4sw4z2yMITIXrgGw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, - "node_modules/ssri": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", - "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", - "dev": true, - "dependencies": { - "minipass": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/ssri/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - }, - "node_modules/tuf-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", - "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", - "dev": true, - "dependencies": { - "@tufjs/models": "1.0.4", - "debug": "^4.3.4", - "make-fetch-happen": "^11.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici": { - "version": "5.26.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", - "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, - "engines": { - "node": ">=14.0" - } - }, - "node_modules/unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" - }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", - "dev": true, - "dependencies": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "configstore": "^6.0.0", - "has-yarn": "^3.0.0", - "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.3.7", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", - "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/widest-line/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/widest-line/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/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==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - }, - "dependencies": { - "@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true - }, - "@actions/core": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", - "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", - "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "@actions/github": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", - "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", - "requires": { - "@actions/http-client": "^2.2.0", - "@octokit/core": "^5.0.1", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/plugin-rest-endpoint-methods": "^10.0.0" - } - }, - "@actions/http-client": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", - "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", - "requires": { - "tunnel": "^0.0.6", - "undici": "^5.25.4" - } - }, - "@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "optional": true - }, - "@eslint-community/eslint-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.2.0.tgz", - "integrity": "sha512-gB8T4H4DEfX2IV9zGDJPOBgP1e/DbfCPDTtEqUMckpvzS1OYtva8JdFYBqMwYk7xAQ429WGF/UPqn8uQ//h2vQ==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^3.3.0" - } - }, - "@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", - "dev": true - }, - "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "@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==", - "dev": true - }, - "@fastify/busboy": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", - "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" - }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, - "@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "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==", - "dev": true - }, - "@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "requires": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.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==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "@mapbox/node-pre-gyp": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", - "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==", - "requires": { - "detect-libc": "^1.0.3", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.5", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - } - }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - } - }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - } - }, - "@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", - "dev": true, - "requires": { - "semver": "^7.3.5" - } - }, - "@npmcli/git": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", - "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", - "dev": true, - "requires": { - "@npmcli/promise-spawn": "^6.0.0", - "lru-cache": "^7.4.4", - "npm-pick-manifest": "^8.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^3.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - }, - "which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@npmcli/installed-package-contents": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", - "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", - "dev": true, - "requires": { - "npm-bundled": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - } - }, - "@npmcli/node-gyp": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", - "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", - "dev": true - }, - "@npmcli/promise-spawn": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", - "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", - "dev": true, - "requires": { - "which": "^3.0.0" - }, - "dependencies": { - "which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@npmcli/run-script": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", - "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", - "dev": true, - "requires": { - "@npmcli/node-gyp": "^3.0.0", - "@npmcli/promise-spawn": "^6.0.0", - "node-gyp": "^9.0.0", - "read-package-json-fast": "^3.0.0", - "which": "^3.0.0" - }, - "dependencies": { - "which": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", - "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==" - }, - "@octokit/core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.1.tgz", - "integrity": "sha512-lyeeeZyESFo+ffI801SaBKmCfsvarO+dgV8/0gD8u1d87clbEdWsP5yC+dSj3zLhb2eIf5SJrn6vDz9AheETHw==", - "requires": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/endpoint": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.1.tgz", - "integrity": "sha512-hRlOKAovtINHQPYHZlfyFwaM8OyetxeoC81lAkBy34uLb8exrZB50SQdeW3EROqiY9G9yxQTpp5OHTV54QD+vA==", - "requires": { - "@octokit/types": "^12.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/graphql": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", - "requires": { - "@octokit/request": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/openapi-types": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.0.tgz", - "integrity": "sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw==" - }, - "@octokit/plugin-paginate-rest": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.0.0.tgz", - "integrity": "sha512-oIJzCpttmBTlEhBmRvb+b9rlnGpmFgDtZ0bB6nq39qIod6A5DP+7RkVLMOixIgRCYSHDTeayWqmiJ2SZ6xgfdw==", - "requires": { - "@octokit/types": "^12.0.0" - } - }, - "@octokit/plugin-rest-endpoint-methods": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.0.1.tgz", - "integrity": "sha512-fgS6HPkPvJiz8CCliewLyym9qAx0RZ/LKh3sATaPfM41y/O2wQ4Z9MrdYeGPVh04wYmHFmWiGlKPC7jWVtZXQA==", - "requires": { - "@octokit/types": "^12.0.0" - } - }, - "@octokit/request": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz", - "integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==", - "requires": { - "@octokit/endpoint": "^9.0.0", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "is-plain-object": "^5.0.0", - "universal-user-agent": "^6.0.0" - } - }, - "@octokit/request-error": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", - "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", - "requires": { - "@octokit/types": "^12.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" - } - }, - "@octokit/types": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.0.0.tgz", - "integrity": "sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg==", - "requires": { - "@octokit/openapi-types": "^19.0.0" - } - }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "optional": true - }, - "@pnpm/config.env-replace": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.0.0.tgz", - "integrity": "sha512-ZVPVDi1E8oeXlYqkGRtX0CkzLTwE2zt62bjWaWKaAvI8NZqHzlMvGeSNDpW+JB3+aKanYb4UETJOF1/CxGPemA==", - "dev": true - }, - "@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "dev": true, - "requires": { - "graceful-fs": "4.2.10" - }, - "dependencies": { - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - } - } - }, - "@pnpm/npm-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.1.0.tgz", - "integrity": "sha512-Oe6ntvgsMTE3hDIqy6sajqHF+MnzJrOF06qC2QSiUEybLL7cp6tjoKUa32gpd9+KPVl4QyMs3E3nsXrx/Vdnlw==", - "dev": true, - "requires": { - "@pnpm/config.env-replace": "^1.0.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - } - }, - "@sigstore/bundle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-1.0.0.tgz", - "integrity": "sha512-yLvrWDOh6uMOUlFCTJIZEnwOT9Xte7NPXUqVexEKGSF5XtBAuSg5du0kn3dRR0p47a4ah10Y0mNt8+uyeQXrBQ==", - "dev": true, - "requires": { - "@sigstore/protobuf-specs": "^0.2.0" - } - }, - "@sigstore/protobuf-specs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.0.tgz", - "integrity": "sha512-8ZhZKAVfXjIspDWwm3D3Kvj0ddbJ0HqDZ/pOs5cx88HpT8mVsotFrg7H1UMnXOuDHz6Zykwxn4mxG3QLuN+RUg==", - "dev": true - }, - "@sigstore/tuf": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.3.tgz", - "integrity": "sha512-2bRovzs0nJZFlCN3rXirE4gwxCn97JNjMmwpecqlbgV9WcxX7WRuIrgzx/X7Ib7MYRbyUTpBYE0s2x6AmZXnlg==", - "dev": true, - "requires": { - "@sigstore/protobuf-specs": "^0.2.0", - "tuf-js": "^1.1.7" - } - }, - "@sindresorhus/is": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", - "integrity": "sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "requires": { - "defer-to-connect": "^2.0.1" - } - }, - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true - }, - "@tufjs/canonical-json": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", - "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", - "dev": true - }, - "@tufjs/models": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", - "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", - "dev": true, - "requires": { - "@tufjs/canonical-json": "1.0.0", - "minimatch": "^9.0.0" - }, - "dependencies": { - "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" - } - } - } - }, - "@types/http-cache-semantics": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", - "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", - "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 - }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" - }, - "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "requires": { - "debug": "4" - } - }, - "agentkeepalive": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", - "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "depd": "^2.0.0", - "humanize-ms": "^1.2.1" - } - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "requires": { - "string-width": "^4.1.0" - } - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" - }, - "are-we-there-yet": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", - "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - } - }, - "array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-string": "^1.0.7" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "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==", - "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" - } - }, - "array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, - "array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, - "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==", - "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", - "is-shared-array-buffer": "^1.0.2" - } - }, - "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 - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" - }, - "boxen": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.2.tgz", - "integrity": "sha512-1Z4UJabXUP1/R9rLpoU3O2lEMnG3pPLAs/ZD2lF3t2q7qD5lM8rqbtnvtvm4N0wEyNlE+9yZVTVAGmd1V5jabg==", - "dev": true, - "requires": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.0", - "chalk": "^5.0.1", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "requires": { - "semver": "^7.0.0" - } - }, - "cacache": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.3.tgz", - "integrity": "sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==", - "dev": true, - "requires": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "dependencies": { - "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" - } - }, - "fs-minipass": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", - "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", - "dev": true, - "requires": { - "minipass": "^5.0.0" - } - }, - "glob": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", - "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - } - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - }, - "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" - } - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true - }, - "cacheable-request": { - "version": "10.2.8", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.8.tgz", - "integrity": "sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==", - "dev": true, - "requires": { - "@types/http-cache-semantics": "^4.0.1", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.2", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "dependencies": { - "mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true - } - } - }, - "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==", - "dev": true, - "requires": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "dev": true - }, - "canvas": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.9.1.tgz", - "integrity": "sha512-vSQti1uG/2gjv3x6QLOZw7TctfufaerTWbVe+NSduHxxLGB+qf3kFgQ6n66DSnuoINtVUjrLLIK2R+lxrBG07A==", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.15.0", - "simple-get": "^3.0.3" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "chart.js": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" - }, - "chartjs-node-canvas": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/chartjs-node-canvas/-/chartjs-node-canvas-4.1.6.tgz", - "integrity": "sha512-UQJbPWrvqB/FoLclGA9BaLQmZbzSYlujF4w8NZd6Xzb+sqgACBb2owDX6m7ifCXLjUW5Nz0Qx0qqrTtQkkSoYw==", - "requires": { - "canvas": "^2.8.0", - "tslib": "^2.3.1" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true - }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "dev": true - }, - "cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", - "dev": true, - "requires": { - "@colors/colors": "1.5.0", - "string-width": "^4.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" - }, - "commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - }, - "dependencies": { - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - } - } - }, - "configstore": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", - "dev": true, - "requires": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "requires": { - "type-fest": "^1.0.1" - }, - "dependencies": { - "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true - } - } - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "requires": { - "ms": "2.1.2" - } - }, - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "requires": { - "mimic-response": "^2.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "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==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - } - }, - "define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "requires": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "deprecation": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "optional": true, - "requires": { - "iconv-lite": "^0.6.2" - } - }, - "env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true - }, - "err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, - "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", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", - "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", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" - } - }, - "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, - "requires": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" - } - }, - "es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": 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", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "dependencies": { - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "requires": { - "is-glob": "^4.0.3" - } - } - } - }, - "eslint-config-standard": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", - "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==", - "dev": true, - "requires": {} - }, - "eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "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==", - "dev": true, - "requires": { - "debug": "^3.2.7" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "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", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", - "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true - } - } - }, - "eslint-plugin-n": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.6.1.tgz", - "integrity": "sha512-R9xw9OtCRxxaxaszTQmQAlPgM+RdGjaL1akWuY/Fv9fRAi8Wj4CUKc6iYVG8QNRjRuo8/BqVYIpfqberJUEacA==", - "dev": true, - "peer": true, - "requires": { - "builtins": "^5.0.1", - "eslint-plugin-es": "^4.1.0", - "eslint-utils": "^3.0.0", - "ignore": "^5.1.1", - "is-core-module": "^2.11.0", - "minimatch": "^3.1.2", - "resolve": "^1.22.1", - "semver": "^7.3.8" - }, - "dependencies": { - "eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "peer": true, - "requires": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "dependencies": { - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "peer": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "peer": true - } - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "peer": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "peer": true - } - } - }, - "eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "requires": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "requires": {} - }, - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "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 - }, - "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "requires": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - } - }, - "esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "exponential-backoff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", - "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", - "dev": true - }, - "fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "requires": { - "reusify": "^1.0.4" - } - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", - "dev": true - }, - "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "requires": { - "is-callable": "^1.1.3" - } - }, - "foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "dependencies": { - "signal-exit": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", - "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", - "dev": true - } - } - }, - "form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "dev": true - }, - "fp-and-or": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/fp-and-or/-/fp-and-or-0.1.4.tgz", - "integrity": "sha512-+yRYRhpnFPWXSly/6V4Lw9IfOV26uu30kynGJ03PW+MnjOEQe45RZ141QcS0aJehYBYA50GfCDnsRbFJdhssRw==", - "dev": true - }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true - }, - "function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gauge": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", - "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.2", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.1", - "object-assign": "^4.1.1", - "signal-exit": "^3.0.0", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.2" - } - }, - "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==", - "dev": true, - "requires": { - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - } - }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "requires": { - "ini": "2.0.0" - }, - "dependencies": { - "ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "dev": true - } - } - }, - "globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3" - } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } - }, - "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.3" - } - }, - "got": { - "version": "12.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.0.tgz", - "integrity": "sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==", - "dev": true, - "requires": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "dependencies": { - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "requires": { - "mimic-response": "^3.1.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true - } - } - }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "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==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.2" - } - }, - "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==", - "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==", - "dev": true - }, - "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, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "dev": true - }, - "hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", - "dev": true, - "requires": { - "function-bind": "^1.1.2" - } - }, - "hosted-git-info": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-5.2.1.tgz", - "integrity": "sha512-xIcQYMnhcx2Nr4JTjsFmwwnr9vldugPy9uVm0o87bjqqWMv9GaqsTeT+i99wTl0mk1uLxJtHxLb8kymqTENQsw==", - "dev": true, - "requires": { - "lru-cache": "^7.5.1" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - } - } - }, - "http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - }, - "http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dev": true, - "requires": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "requires": { - "ms": "^2.0.0" - } - }, - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - }, - "ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true - }, - "ignore-walk": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", - "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", - "dev": true, - "requires": { - "minimatch": "^9.0.0" - }, - "dependencies": { - "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" - } - } - } - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - } - }, - "ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "requires": { - "ci-info": "^3.2.0" - } - }, - "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==", - "dev": true, - "requires": { - "hasown": "^2.0.0" - } - }, - "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, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "dev": true, - "requires": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - } - }, - "is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "dev": true - }, - "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==", - "dev": true - }, - "is-npm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "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==", - "dev": true, - "requires": { - "which-typed-array": "^1.1.11" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "dev": true - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dev": true, - "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" - } - }, - "jju": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", - "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", - "dev": true - }, - "json-parse-helpfulerror": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/json-parse-helpfulerror/-/json-parse-helpfulerror-1.0.3.tgz", - "integrity": "sha512-XgP0FGR77+QhUxjXkwOMkC94k3WtqEBfcnjWqhRd82qTat4SWKRE+9kUnynz/shm3I4ea2+qISvTIeGTNU7kJg==", - "dev": true, - "requires": { - "jju": "^1.1.0" - } - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - }, - "jsonlines": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsonlines/-/jsonlines-0.1.1.tgz", - "integrity": "sha512-ekDrAGso79Cvf+dtm+mL8OBI2bmAOt3gssYs833De/C9NmIpWDWyUO4zPgB5x2/OhY366dkhgfPMYfwZF7yOZA==", - "dev": true - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", - "dev": true - }, - "keyv": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz", - "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==", - "dev": true, - "requires": { - "json-buffer": "3.0.1" - } - }, - "kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true - }, - "latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "dev": true, - "requires": { - "package-json": "^8.1.0" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true - }, - "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==", - "requires": { - "yallist": "^4.0.0" - } - }, - "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "requires": { - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" - } - } - }, - "make-fetch-happen": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", - "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", - "dev": true, - "requires": { - "agentkeepalive": "^4.2.1", - "cacache": "^17.0.0", - "http-cache-semantics": "^4.1.1", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^7.7.1", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^7.0.0", - "ssri": "^10.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true - }, - "minipass": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", - "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", - "requires": { - "yallist": "^4.0.0" - } - }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-fetch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", - "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", - "dev": true, - "requires": { - "encoding": "^0.1.13", - "minipass": "^5.0.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-json-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", - "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", - "dev": true, - "requires": { - "jsonparse": "^1.3.1", - "minipass": "^3.0.0" - } - }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "requires": { - "minipass": "^3.0.0" - } - }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "requires": { - "whatwg-url": "^5.0.0" - } - }, - "node-gyp": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz", - "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==", - "dev": true, - "requires": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^11.0.3", - "nopt": "^6.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "dependencies": { - "are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - } - }, - "gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "dev": true, - "requires": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - } - }, - "nopt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", - "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", - "dev": true, - "requires": { - "abbrev": "^1.0.0" - } - }, - "npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "dev": true, - "requires": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - } - } - } - }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, - "normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", - "dev": true, - "requires": { - "hosted-git-info": "^6.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "dependencies": { - "hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "requires": { - "lru-cache": "^7.5.1" - } - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - } - } - }, - "normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "dev": true - }, - "npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", - "dev": true, - "requires": { - "npm-normalize-package-bin": "^3.0.0" - } - }, - "npm-check-updates": { - "version": "16.14.15", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", - "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", - "dev": true, - "requires": { - "chalk": "^5.3.0", - "cli-table3": "^0.6.3", - "commander": "^10.0.1", - "fast-memoize": "^2.5.2", - "find-up": "5.0.0", - "fp-and-or": "^0.1.4", - "get-stdin": "^8.0.0", - "globby": "^11.0.4", - "hosted-git-info": "^5.1.0", - "ini": "^4.1.1", - "js-yaml": "^4.1.0", - "json-parse-helpfulerror": "^1.0.3", - "jsonlines": "^0.1.1", - "lodash": "^4.17.21", - "make-fetch-happen": "^11.1.1", - "minimatch": "^9.0.3", - "p-map": "^4.0.0", - "pacote": "15.2.0", - "parse-github-url": "^1.0.2", - "progress": "^2.0.3", - "prompts-ncu": "^3.0.0", - "rc-config-loader": "^4.1.3", - "remote-git-tags": "^3.0.0", - "rimraf": "^5.0.5", - "semver": "^7.5.4", - "semver-utils": "^1.1.4", - "source-map-support": "^0.5.21", - "spawn-please": "^2.0.2", - "strip-ansi": "^7.1.0", - "strip-json-comments": "^5.0.1", - "untildify": "^4.0.0", - "update-notifier": "^6.0.2" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "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" - } - }, - "chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true - }, - "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - } - }, - "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" - } - }, - "minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "dev": true - }, - "rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", - "dev": true, - "requires": { - "glob": "^10.3.7" - } - }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - }, - "strip-json-comments": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.1.tgz", - "integrity": "sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==", - "dev": true - } - } - }, - "npm-install-checks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", - "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", - "dev": true, - "requires": { - "semver": "^7.1.1" - } - }, - "npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true - }, - "npm-package-arg": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", - "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", - "dev": true, - "requires": { - "hosted-git-info": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", - "dev": true, - "requires": { - "lru-cache": "^7.5.1" - } - }, - "lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true - } - } - }, - "npm-packlist": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", - "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", - "dev": true, - "requires": { - "ignore-walk": "^6.0.0" - } - }, - "npm-pick-manifest": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", - "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", - "dev": true, - "requires": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^10.0.0", - "semver": "^7.3.5" - } - }, - "npm-registry-fetch": { - "version": "14.0.5", - "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", - "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", - "dev": true, - "requires": { - "make-fetch-happen": "^11.0.0", - "minipass": "^5.0.0", - "minipass-fetch": "^3.0.0", - "minipass-json-stream": "^1.0.1", - "minizlib": "^2.1.2", - "npm-package-arg": "^10.0.0", - "proc-log": "^3.0.0" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "npmlog": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", - "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", - "requires": { - "are-we-there-yet": "^2.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^3.0.0", - "set-blocking": "^2.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "dev": true - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, - "object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" - } - }, - "object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "requires": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - } - }, - "p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "package-json": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.0.tgz", - "integrity": "sha512-hySwcV8RAWeAfPsXb9/HGSPn8lwDnv6fabH+obUZKX169QknRkRhPxd1yMubpKDskLFATkl3jHpNtVtDPFA0Wg==", - "dev": true, - "requires": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - } - }, - "pacote": { - "version": "15.2.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", - "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", - "dev": true, - "requires": { - "@npmcli/git": "^4.0.0", - "@npmcli/installed-package-contents": "^2.0.1", - "@npmcli/promise-spawn": "^6.0.1", - "@npmcli/run-script": "^6.0.0", - "cacache": "^17.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^5.0.0", - "npm-package-arg": "^10.0.0", - "npm-packlist": "^7.0.0", - "npm-pick-manifest": "^8.0.0", - "npm-registry-fetch": "^14.0.0", - "proc-log": "^3.0.0", - "promise-retry": "^2.0.1", - "read-package-json": "^6.0.0", - "read-package-json-fast": "^3.0.0", - "sigstore": "^1.3.0", - "ssri": "^10.0.0", - "tar": "^6.1.11" - }, - "dependencies": { - "fs-minipass": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", - "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", - "dev": true, - "requires": { - "minipass": "^5.0.0" - } - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-github-url": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", - "integrity": "sha512-kgBf6avCbO3Cn6+RnzRGLkUsv4ZVqv/VfAYkRsyBcgkshNvVBkRn1FEZcW0Jb+npXQWm2vHPnnOqFteZxRRGNw==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dev": true, - "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", - "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", - "dev": true - }, - "minipass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz", - "integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==", - "dev": true - } - } - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "requires": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - } - }, - "prompts-ncu": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prompts-ncu/-/prompts-ncu-3.0.0.tgz", - "integrity": "sha512-qyz9UxZ5MlPKWVhWrCmSZ1ahm2GVYdjLb8og2sg0IPth1KRuhcggHGuijz0e41dkx35p1t1q3GRISGH7QGALFA==", - "dev": true, - "requires": { - "kleur": "^4.0.1", - "sisteransi": "^1.0.5" - } - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true - }, - "punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true - }, - "pupa": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", - "dev": true, - "requires": { - "escape-goat": "^4.0.0" - } - }, - "queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true - }, - "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - } - } - }, - "rc-config-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/rc-config-loader/-/rc-config-loader-4.1.3.tgz", - "integrity": "sha512-kD7FqML7l800i6pS6pvLyIE2ncbk9Du8Q0gp/4hMPhJU6ZxApkoLcGD8ZeqgiAlfwZ6BlETq6qqe+12DUL207w==", - "dev": true, - "requires": { - "debug": "^4.3.4", - "js-yaml": "^4.1.0", - "json5": "^2.2.2", - "require-from-string": "^2.0.2" - }, - "dependencies": { - "json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true - } - } - }, - "read-package-json": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", - "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", - "dev": true, - "requires": { - "glob": "^10.2.2", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^5.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "dependencies": { - "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" - } - }, - "glob": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.3.tgz", - "integrity": "sha512-92vPiMb/iqpmEgsOoIDvTjc50wf9CCCvMzsi6W0JLPeUKE8TWP1a73PgqSrqy7iAZxaSD1YdzU7QZR5LF51MJw==", - "dev": true, - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - } - }, - "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" - } - }, - "minipass": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.2.tgz", - "integrity": "sha512-eL79dXrE1q9dBbDCLg7xfn/vl7MS4F1gvJAgjJrQli/jbQWdUttuVawphqpffoIYfRdq78LHx6GP4bU/EQ2ATA==", - "dev": true - } - } - }, - "read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "requires": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "registry-auth-token": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", - "dev": true, - "requires": { - "@pnpm/npm-conf": "^2.1.0" - } - }, - "registry-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", - "dev": true, - "requires": { - "rc": "1.2.8" - } - }, - "remote-git-tags": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remote-git-tags/-/remote-git-tags-3.0.0.tgz", - "integrity": "sha512-C9hAO4eoEsX+OXA4rla66pXZQ+TLQ8T9dttgQj18yuKlPMTVkIkdYXvlMC55IuUsIkV6DpmQYi10JKFLaU+l7w==", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", - "dev": true, - "requires": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "requires": { - "lowercase-keys": "^3.0.0" - } - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "requires": { - "queue-microtask": "^1.2.2" - } - }, - "safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true - }, - "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==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "dev": true, - "requires": { - "semver": "^7.3.5" - } - }, - "semver-utils": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/semver-utils/-/semver-utils-1.1.4.tgz", - "integrity": "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", - "dev": true, - "requires": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" - } - }, - "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==", - "dev": true, - "requires": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "sigstore": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.8.0.tgz", - "integrity": "sha512-ogU8qtQ3VFBawRJ8wjsBEX/vIFeHuGs1fm4jZtjWQwjo8pfAt7T/rh+udlAN4+QUe0IzA8qRSc/YZ7dHP6kh+w==", - "dev": true, - "requires": { - "@sigstore/bundle": "^1.0.0", - "@sigstore/protobuf-specs": "^0.2.0", - "@sigstore/tuf": "^1.0.3", - "make-fetch-happen": "^11.0.1" - } - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "simple-get": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", - "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true - }, - "socks": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", - "dev": true, - "requires": { - "ip": "^2.0.0", - "smart-buffer": "^4.2.0" - } - }, - "socks-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", - "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", - "dev": true, - "requires": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "spawn-please": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/spawn-please/-/spawn-please-2.0.2.tgz", - "integrity": "sha512-KM8coezO6ISQ89c1BzyWNtcn2V2kAVtwIXd3cN/V5a0xPYc1F/vydrRc01wsKFEQ/p+V1a4sw4z2yMITIXrgGw==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3" - } - }, - "spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", - "dev": true - }, - "ssri": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", - "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", - "dev": true, - "requires": { - "minipass": "^5.0.0" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true - } - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "@octokit/core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.1.tgz", + "integrity": "sha512-lyeeeZyESFo+ffI801SaBKmCfsvarO+dgV8/0gD8u1d87clbEdWsP5yC+dSj3zLhb2eIf5SJrn6vDz9AheETHw==", "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "@octokit/endpoint": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.1.tgz", + "integrity": "sha512-hRlOKAovtINHQPYHZlfyFwaM8OyetxeoC81lAkBy34uLb8exrZB50SQdeW3EROqiY9G9yxQTpp5OHTV54QD+vA==", "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "@octokit/types": "^12.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" } }, - "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==", - "dev": true, + "@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - } + "@octokit/openapi-types": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.0.tgz", + "integrity": "sha512-PclQ6JGMTE9iUStpzMkwLCISFn/wDeRjkZFIKALpvJQNBGwDoYYi2fFvuHwssoQ1rXI5mfh6jgTgWuddeUzfWw==" }, - "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==", - "dev": true, + "@octokit/plugin-paginate-rest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.0.0.tgz", + "integrity": "sha512-oIJzCpttmBTlEhBmRvb+b9rlnGpmFgDtZ0bB6nq39qIod6A5DP+7RkVLMOixIgRCYSHDTeayWqmiJ2SZ6xgfdw==", "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "@octokit/types": "^12.0.0" } }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "@octokit/plugin-rest-endpoint-methods": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.0.1.tgz", + "integrity": "sha512-fgS6HPkPvJiz8CCliewLyym9qAx0RZ/LKh3sATaPfM41y/O2wQ4Z9MrdYeGPVh04wYmHFmWiGlKPC7jWVtZXQA==", "requires": { - "ansi-regex": "^5.0.1" + "@octokit/types": "^12.0.0" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "@octokit/request": { + "version": "8.1.4", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.4.tgz", + "integrity": "sha512-M0aaFfpGPEKrg7XoA/gwgRvc9MSXHRO2Ioki1qrPDbl1e9YhjIwVoHE7HIKmv/m3idzldj//xBujcFNqGX6ENA==", "requires": { - "ansi-regex": "^5.0.1" + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, + "@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", "requires": { - "has-flag": "^4.0.0" + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" } }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "@octokit/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.0.0.tgz", + "integrity": "sha512-EzD434aHTFifGudYAygnFlS1Tl6KhbTynEWELQXIbTY8Msvb5nEqTZIm7sbPEt4mQYLZwu3zPKVdeIrw0g7ovg==", "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@octokit/openapi-types": "^19.0.0" } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true + "before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" }, - "tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" }, - "tuf-js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", - "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", - "dev": true, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { - "@tufjs/models": "1.0.4", - "debug": "^4.3.4", - "make-fetch-happen": "^11.1.1" + "wrappy": "1" } }, "tunnel": { @@ -10439,89 +412,6 @@ "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" - } - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - } - }, - "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==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" - } - }, - "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==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - } - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, "undici": { "version": "5.26.4", "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", @@ -10530,307 +420,20 @@ "@fastify/busboy": "^2.0.0" } }, - "unique-filename": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", - "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", - "dev": true, - "requires": { - "unique-slug": "^4.0.0" - } - }, - "unique-slug": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", - "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "requires": { - "crypto-random-string": "^4.0.0" - } - }, "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" }, - "untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true - }, - "update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", - "dev": true, - "requires": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "configstore": "^6.0.0", - "has-yarn": "^3.0.0", - "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.3.7", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "dependencies": { - "chalk": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.2.0.tgz", - "integrity": "sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==", - "dev": true - } - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", - "dev": true, - "requires": { - "builtins": "^5.0.0" - } - }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "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==", - "dev": true, - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - } - }, - "wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "requires": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "dev": true, - "requires": { - "string-width": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "requires": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true - }, - "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true - }, - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true - }, - "string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "requires": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - } - }, - "strip-ansi": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", - "dev": true, - "requires": { - "ansi-regex": "^6.0.1" - } - } - } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "dev": true - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true } } } diff --git a/dev/coverage-action/package.json b/dev/coverage-action/package.json index e758561e44..0a8794d2a2 100644 --- a/dev/coverage-action/package.json +++ b/dev/coverage-action/package.json @@ -8,17 +8,7 @@ "dependencies": { "@actions/core": "1.10.1", "@actions/github": "6.0.0", - "chart.js": "3.9.1", - "chartjs-node-canvas": "4.1.6", "lodash": "4.17.21", "luxon": "3.4.4" - }, - "devDependencies": { - "eslint": "8.57.0", - "eslint-config-standard": "17.1.0", - "eslint-plugin-import": "2.29.1", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-promise": "6.1.1", - "npm-check-updates": "16.14.15" } } diff --git a/dev/deploy-to-container/cli.js b/dev/deploy-to-container/cli.js index 2912c98fdf..4aee7ba1ed 100644 --- a/dev/deploy-to-container/cli.js +++ b/dev/deploy-to-container/cli.js @@ -3,7 +3,7 @@ import Docker from 'dockerode' import path from 'path' import fs from 'fs-extra' -import tar from 'tar' +import * as tar from 'tar' import yargs from 'yargs/yargs' import { hideBin } from 'yargs/helpers' import slugify from 'slugify' diff --git a/dev/deploy-to-container/package-lock.json b/dev/deploy-to-container/package-lock.json index 113c6e8329..7b86986d56 100644 --- a/dev/deploy-to-container/package-lock.json +++ b/dev/deploy-to-container/package-lock.json @@ -8,10 +8,10 @@ "dependencies": { "dockerode": "^4.0.2", "fs-extra": "^11.2.0", - "nanoid": "5.0.6", + "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^6.2.0", + "tar": "^7.4.0", "yargs": "^17.7.2" }, "engines": { @@ -23,6 +23,115 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -53,6 +162,11 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -80,6 +194,14 @@ "tweetnacl": "^0.14.3" } }, + "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==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -137,6 +259,19 @@ "node": ">=10.0.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -180,6 +315,11 @@ "node": ">= 8.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -201,6 +341,21 @@ "node": ">=6" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -219,17 +374,6 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -238,6 +382,27 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -275,6 +440,28 @@ "node": ">=8" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -286,38 +473,60 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mkdirp-classic": { @@ -337,9 +546,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", - "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", "funding": [ { "type": "github", @@ -366,6 +575,29 @@ "wrappy": "1" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -396,6 +628,23 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -420,6 +669,36 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slugify": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", @@ -471,6 +750,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -482,20 +775,32 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -558,19 +863,11 @@ } }, "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/tweetnacl": { @@ -591,6 +888,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -607,6 +918,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -621,9 +949,12 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } }, "node_modules/yargs": { "version": "17.7.2", @@ -657,6 +988,78 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.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==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "requires": { + "minipass": "^7.0.4" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -678,6 +1081,11 @@ "safer-buffer": "~2.1.0" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -691,6 +1099,14 @@ "tweetnacl": "^0.14.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==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -735,6 +1151,16 @@ "nan": "^2.17.0" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -764,6 +1190,11 @@ "tar-fs": "~2.0.1" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -782,6 +1213,15 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -797,19 +1237,23 @@ "universalify": "^2.0.0" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -830,6 +1274,20 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -839,27 +1297,37 @@ "universalify": "^2.0.0" } }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + }, + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "requires": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" } }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" } }, "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" }, "mkdirp-classic": { "version": "0.5.3", @@ -878,9 +1346,9 @@ "optional": true }, "nanoid": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", - "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==" + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==" }, "nanoid-dictionary": { "version": "5.0.0-beta.1", @@ -895,6 +1363,20 @@ "wrappy": "1" } }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -919,6 +1401,14 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "requires": { + "glob": "^10.3.7" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -929,6 +1419,24 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, "slugify": { "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", @@ -968,6 +1476,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -976,28 +1494,31 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "dependencies": { "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" } } }, @@ -1060,6 +1581,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -1070,6 +1599,16 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1081,9 +1620,9 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" }, "yargs": { "version": "17.7.2", diff --git a/dev/deploy-to-container/package.json b/dev/deploy-to-container/package.json index f4b7320e54..2f582dd2a7 100644 --- a/dev/deploy-to-container/package.json +++ b/dev/deploy-to-container/package.json @@ -4,10 +4,10 @@ "dependencies": { "dockerode": "^4.0.2", "fs-extra": "^11.2.0", - "nanoid": "5.0.6", + "nanoid": "5.0.7", "nanoid-dictionary": "5.0.0-beta.1", "slugify": "1.6.6", - "tar": "^6.2.0", + "tar": "^7.4.0", "yargs": "^17.7.2" }, "engines": { diff --git a/dev/deploy-to-container/settings_local.py b/dev/deploy-to-container/settings_local.py index 15b44433ea..25eacc3004 100644 --- a/dev/deploy-to-container/settings_local.py +++ b/dev/deploy-to-container/settings_local.py @@ -60,10 +60,11 @@ BOFREQ_PATH = '/assets/ietf-ftp/bofreq/' CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/' STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/' -INTERNET_DRAFT_ARCHIVE_DIR = '/assets/archive/id' +INTERNET_DRAFT_ARCHIVE_DIR = '/assets/collection/draft-archive' INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/archive/id' BIBXML_BASE_PATH = '/assets/ietfdata/derived/bibxml' IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH +FTP_DIR = '/assets/ftp' NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/' SLIDE_STAGING_PATH = '/test/staging/' diff --git a/dev/diff/package-lock.json b/dev/diff/package-lock.json index b8f007baf1..3500ccec48 100644 --- a/dev/diff/package-lock.json +++ b/dev/diff/package-lock.json @@ -17,7 +17,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "pretty-bytes": "^6.1.1", - "tar": "^6.2.0", + "tar": "^7.4.0", "yargs": "^17.7.2" }, "engines": { @@ -29,6 +29,115 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/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==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@sindresorhus/is": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", @@ -123,6 +232,11 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -150,6 +264,14 @@ "tweetnacl": "^0.14.3" } }, + "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==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -331,6 +453,19 @@ "node": ">=10.0.0" } }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -491,6 +626,32 @@ "pend": "~1.2.0" } }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/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==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -517,17 +678,6 @@ "node": ">=14.14" } }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -547,6 +697,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/got": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", @@ -625,6 +796,28 @@ "node": ">=8" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -858,6 +1051,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "engines": { + "node": "14 || >=16.14" + } + }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -885,38 +1086,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": ">= 8" + "node": ">= 18" } }, "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "bin": { - "mkdirp": "bin/cmd.js" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mkdirp-classic": { @@ -976,6 +1191,29 @@ "node": ">=12.20" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -1072,6 +1310,23 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, + "node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1096,6 +1351,25 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -1181,6 +1455,20 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -1192,20 +1480,32 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/tar-fs": { @@ -1268,19 +1568,11 @@ } }, "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/tweetnacl": { @@ -1312,6 +1604,20 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -1328,6 +1634,23 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1342,9 +1665,12 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } }, "node_modules/yargs": { "version": "17.7.2", @@ -1387,6 +1713,78 @@ "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==" }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.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==" + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "requires": { + "minipass": "^7.0.4" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, "@sindresorhus/is": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.3.0.tgz", @@ -1454,6 +1852,11 @@ "safer-buffer": "~2.1.0" } }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1467,6 +1870,14 @@ "tweetnacl": "^0.14.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==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -1592,6 +2003,16 @@ "nan": "^2.17.0" } }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1707,6 +2128,22 @@ "pend": "~1.2.0" } }, + "foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + } + } + }, "form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -1727,14 +2164,6 @@ "universalify": "^2.0.0" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "requires": { - "minipass": "^3.0.0" - } - }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1745,6 +2174,18 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, + "glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + } + }, "got": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", @@ -1797,6 +2238,20 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -1949,6 +2404,11 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==" }, + "lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==" + }, "luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -1964,27 +2424,32 @@ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==" }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "requires": { - "yallist": "^4.0.0" + "brace-expansion": "^2.0.1" } }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" } }, "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" }, "mkdirp-classic": { "version": "0.5.3", @@ -2028,6 +2493,20 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==" }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "path-scurry": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -2094,6 +2573,14 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, + "rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "requires": { + "glob": "^10.3.7" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2104,6 +2591,19 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -2164,6 +2664,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2172,28 +2682,31 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.0.tgz", + "integrity": "sha512-XQs0S8fuAkQWuqhDeCdMlJXDX80D7EOVLDPVFkna9yQfzS+PHKgfxcei0jf6/+QAWcjqrnC8uM3fSAnrQl+XYg==", "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "dependencies": { "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" } } }, @@ -2261,6 +2774,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2271,6 +2792,16 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2282,9 +2813,9 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" }, "yargs": { "version": "17.7.2", diff --git a/dev/diff/package.json b/dev/diff/package.json index cd59f2655d..1b5540e346 100644 --- a/dev/diff/package.json +++ b/dev/diff/package.json @@ -13,7 +13,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "pretty-bytes": "^6.1.1", - "tar": "^6.2.0", + "tar": "^7.4.0", "yargs": "^17.7.2" }, "engines": { diff --git a/dev/diff/settings_local.py b/dev/diff/settings_local.py index 593ccadd7f..774c7797cf 100644 --- a/dev/diff/settings_local.py +++ b/dev/diff/settings_local.py @@ -57,9 +57,10 @@ BOFREQ_PATH = '/assets/ietf-ftp/bofreq/' CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/' STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/' -INTERNET_DRAFT_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' +INTERNET_DRAFT_ARCHIVE_DIR = '/assets/collection/draft-archive' INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' BIBXML_BASE_PATH = '/assets/ietfdata/derived/bibxml' +FTP_DIR = '/assets/ftp' NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/' SLIDE_STAGING_PATH = 'test/staging/' diff --git a/dev/legacy/notes/notes.html b/dev/legacy/notes/notes.html index 85980a5b1b..cb10a18689 100644 --- a/dev/legacy/notes/notes.html +++ b/dev/legacy/notes/notes.html @@ -355,7 +355,7 @@

Introduction

in one place.

With my recent investigations of code analysis tools, I thought it might be a good idea to start collecting these in one place for the project.

-
+
Henrik <henrik@levkowetz.com>, 23 Mar 2014
@@ -398,8 +398,9 @@

PyChecker

do the right thing, but once it was made to run on the datatracker code, and ignore the django code, it didn't report anything that PyFlakes hadn't already caught.

-
-Henrik <henrik@levkowetz.com>, 23 Mar 2014
+
+ Henrik <henrik@levkowetz.com>, 23 Mar 2014 +
diff --git a/dev/tests/settings_local.py b/dev/tests/settings_local.py index 0cd761c0a9..8b5d90b1ec 100644 --- a/dev/tests/settings_local.py +++ b/dev/tests/settings_local.py @@ -56,9 +56,10 @@ BOFREQ_PATH = '/assets/ietf-ftp/bofreq/' CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/' STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/' -INTERNET_DRAFT_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' +INTERNET_DRAFT_ARCHIVE_DIR = '/assets/collection/draft-archive' INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' BIBXML_BASE_PATH = '/assets/ietfdata/derived/bibxml' +FTP_DIR = '/assets/ftp' NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/' SLIDE_STAGING_PATH = 'test/staging/' diff --git a/docker/README.md b/docker/README.md index bc9af7c212..2e4b50d8ff 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,7 +6,7 @@ > See the [IETF Tools Windows Dev guide](https://github.com/ietf-tools/.github/blob/main/docs/windows-dev.md) on how to get started when using Windows. -2. On Linux, you must also install [Docker Compose](https://docs.docker.com/compose/install/). Docker Desktop for Mac and Windows already include Docker Compose. +2. On Linux, you must [install Docker Compose manually](https://docs.docker.com/compose/install/linux/#install-the-plugin-manually) and not install Docker Desktop. On Mac and Windows install Docker Desktop which already includes Docker Compose. 2. If you have a copy of the datatracker code checked out already, simply `cd` to the top-level directory. @@ -183,3 +183,18 @@ The content of the source files will be copied into the target `.ics` files. Mak ### Missing assets in the data folder Because including all assets in the image would significantly increase the file size, they are not included by default. You can however fetch them by running the **Fetch assets via rsync** task in VS Code or run manually the script `docker/scripts/app-rsync-extras.sh` + + +### Linux file permissions leaking to the host system + +If on the host filesystem you have permissions that look like this, + +```bash +$ ls -la +total 4624 +drwxrwxr-x 2 100999 100999 4096 May 25 07:56 bin +drwxrwxr-x 5 100999 100999 4096 May 25 07:56 client +(etc...) +``` + +Try uninstalling Docker Desktop and installing Docker Compose manually. The Docker Compose bundled with Docker Desktop is incompatible with our software. See also [Rootless Docker: file ownership changes #3343](https://github.com/lando/lando/issues/3343), [Docker context desktop-linux has container permission issues #75](https://github.com/docker/desktop-linux/issues/75). \ No newline at end of file diff --git a/docker/configs/settings_local.py b/docker/configs/settings_local.py index 07c16c2e9a..bcd04898ea 100644 --- a/docker/configs/settings_local.py +++ b/docker/configs/settings_local.py @@ -46,10 +46,11 @@ BOFREQ_PATH = '/assets/ietf-ftp/bofreq/' CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/' STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/' -INTERNET_DRAFT_ARCHIVE_DIR = '/assets/archive/id' +INTERNET_DRAFT_ARCHIVE_DIR = '/assets/collection/draft-archive' INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/archive/id' BIBXML_BASE_PATH = '/assets/ietfdata/derived/bibxml' IDSUBMIT_REPOSITORY_PATH = INTERNET_DRAFT_PATH +FTP_DIR = '/assets/ftp' NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/' SLIDE_STAGING_PATH = 'test/staging/' diff --git a/docker/db.Dockerfile b/docker/db.Dockerfile index 58d7f2728c..31c41011e6 100644 --- a/docker/db.Dockerfile +++ b/docker/db.Dockerfile @@ -1,7 +1,7 @@ # ===================== # --- Builder Stage --- # ===================== -FROM postgres:14.6 AS builder +FROM postgres:16 AS builder ENV POSTGRES_PASSWORD=hk2j22sfiv ENV POSTGRES_USER=django @@ -19,7 +19,7 @@ RUN ["/usr/local/bin/docker-entrypoint.sh", "postgres"] # =================== # --- Final Image --- # =================== -FROM postgres:14.6 +FROM postgres:16 LABEL maintainer="IETF Tools Team " COPY --from=builder /data $PGDATA diff --git a/docker/scripts/app-create-dirs.sh b/docker/scripts/app-create-dirs.sh index d9296ecffe..b75c57767d 100755 --- a/docker/scripts/app-create-dirs.sh +++ b/docker/scripts/app-create-dirs.sh @@ -9,6 +9,8 @@ for sub in \ test/wiki/ietf \ data/nomcom_keys/public_keys \ /assets/archive/id \ + /assets/collection \ + /assets/collection/draft-archive \ /assets/ietf-ftp \ /assets/ietf-ftp/bofreq \ /assets/ietf-ftp/charter \ @@ -33,6 +35,10 @@ for sub in \ /assets/www6/iesg \ /assets/www6/iesg/evaluation \ /assets/media/photo \ + /assets/ftp \ + /assets/ftp/charter \ + /assets/ftp/internet-drafts \ + /assets/ftp/review \ ; do if [ ! -d "$sub" ]; then echo "Creating dir $sub" diff --git a/docker/scripts/app-init.sh b/docker/scripts/app-init.sh index c8286b2429..b96b88f1f5 100755 --- a/docker/scripts/app-init.sh +++ b/docker/scripts/app-init.sh @@ -2,6 +2,12 @@ WORKSPACEDIR="/workspace" +# Handle Linux host mounting the workspace dir as root +if [ ! -O "${WORKSPACEDIR}/ietf" ]; then + sudo chown -R dev:dev $WORKSPACEDIR +fi + +# Start rsyslog service sudo service rsyslog start &>/dev/null # Add /workspace as a safe git directory diff --git a/helm/.helmignore b/helm/.helmignore deleted file mode 100644 index 2252590f2e..0000000000 --- a/helm/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ \ No newline at end of file diff --git a/helm/Chart.yaml b/helm/Chart.yaml deleted file mode 100644 index 1c19834c06..0000000000 --- a/helm/Chart.yaml +++ /dev/null @@ -1,23 +0,0 @@ -apiVersion: v2 -name: datatracker -description: The day-to-day front-end to the IETF database for people who work on IETF standards. -home: https://datatracker.ietf.org -sources: - - https://github.com/ietf-tools/datatracker -maintainers: - - name: IETF Tools Team - email: tools-discuss@ietf.org - url: https://github.com/ietf-tools - -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.0.0" \ No newline at end of file diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl deleted file mode 100644 index 071e9b824c..0000000000 --- a/helm/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* - Expand the name of the chart. - */}} -{{- define "datatracker.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define "datatracker.fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "datatracker.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "datatracker.labels" -}} -helm.sh/chart: {{ include "datatracker.chart" . }} -{{ include "datatracker.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "datatracker.selectorLabels" -}} -app.kubernetes.io/name: {{ include "datatracker.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "datatracker.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "datatracker.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml deleted file mode 100644 index b47c41a970..0000000000 --- a/helm/templates/deployment.yaml +++ /dev/null @@ -1,66 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "datatracker.fullname" . }} - labels: - {{- include "datatracker.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - revisionHistoryLimit: {{ .Values.revisionHistoryLimit }} - selector: - matchLabels: - {{- include "datatracker.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "datatracker.selectorLabels" . | nindent 8 }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "datatracker.serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ default "latest" .Values.image.tag }}" - imagePullPolicy: {{ default "IfNotPresent" .Values.image.imagePullPolicy }} - env: - {{- if .Values.env }} - {{- toYaml .Values.env | nindent 12 }} - {{- end }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - ports: - - name: http - containerPort: 8000 - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe | nindent 12 }} - startupProbe: - {{- toYaml .Values.startupProbe | nindent 12 }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} diff --git a/helm/templates/hpa.yaml b/helm/templates/hpa.yaml deleted file mode 100644 index 518f7e23ab..0000000000 --- a/helm/templates/hpa.yaml +++ /dev/null @@ -1,32 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include "datatracker.fullname" . }} - labels: - {{- include "datatracker.labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include "datatracker.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/templates/ingress.yaml b/helm/templates/ingress.yaml deleted file mode 100644 index 8d9258cd83..0000000000 --- a/helm/templates/ingress.yaml +++ /dev/null @@ -1,61 +0,0 @@ -{{- if .Values.ingress.enabled -}} -{{- $fullName := include "datatracker.fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "datatracker.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} \ No newline at end of file diff --git a/helm/templates/service.yaml b/helm/templates/service.yaml deleted file mode 100644 index f1bdca0ad2..0000000000 --- a/helm/templates/service.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{include "datatracker.fullname" .}} - labels: {{- include "datatracker.labels" . | nindent 4 }} - {{- with .Values.service.annotations }} - annotations: - {{- range $key, $value := . }} - {{ $key }}: {{ $value | quote }} - {{- end }} - {{- end }} -spec: - type: {{.Values.service.type}} - ports: - - port: {{ default "80" .Values.service.port}} - targetPort: http - protocol: TCP - name: http - selector: {{- include "datatracker.selectorLabels" . | nindent 4}} \ No newline at end of file diff --git a/helm/templates/serviceaccount.yaml b/helm/templates/serviceaccount.yaml deleted file mode 100644 index 475fcd51f7..0000000000 --- a/helm/templates/serviceaccount.yaml +++ /dev/null @@ -1,12 +0,0 @@ -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "datatracker.serviceAccountName" . }} - labels: - {{- include "datatracker.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end -}} \ No newline at end of file diff --git a/helm/values.yaml b/helm/values.yaml deleted file mode 100644 index 92efbce9dd..0000000000 --- a/helm/values.yaml +++ /dev/null @@ -1,118 +0,0 @@ -# Default values for datatracker. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: "ghcr.io/ietf-tools/datatracker" - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - # tag: "v1.1.0" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -livenessProbe: - httpGet: - path: /healthz - port: http - -readinessProbe: - httpGet: - path: /healthz - port: http - -startupProbe: - initialDelaySeconds: 15 - periodSeconds: 5 - timeoutSeconds: 5 - successThreshold: 1 - failureThreshold: 60 - httpGet: - path: /healthz - port: http - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 80 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: datatracker.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} \ No newline at end of file diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 2310d71d75..fd8eb52cd6 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -1,6 +1,6 @@ # Copyright The IETF Trust 2015-2020, All Rights Reserved # -*- coding: utf-8 -*- - +import base64 import datetime import json import html @@ -10,6 +10,7 @@ from importlib import import_module from pathlib import Path +from random import randrange from django.apps import apps from django.conf import settings @@ -30,17 +31,18 @@ from ietf.group.factories import RoleFactory from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.models import Session -from ietf.nomcom.models import Volunteer, NomCom +from ietf.nomcom.models import Volunteer from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year from ietf.person.factories import PersonFactory, random_faker, EmailFactory from ietf.person.models import Email, User from ietf.person.models import PersonalApiKey from ietf.stats.models import MeetingRegistration -from ietf.utils.mail import outbox, get_payload_text +from ietf.utils.mail import empty_outbox, outbox, get_payload_text from ietf.utils.models import DumpInfo from ietf.utils.test_utils import TestCase, login_testing_unauthorized, reload_db_objects from .ietf_utils import is_valid_token, requires_api_token +from .views import EmailIngestionError OMITTED_APPS = ( 'ietf.secr.meetings', @@ -827,7 +829,7 @@ def test_api_new_meeting_registration_nomcom_volunteer(self): 'reg_type': 'onsite', 'ticket_type': '', 'checkedin': 'False', - 'is_nomcom_volunteer': 'True', + 'is_nomcom_volunteer': 'False', } person = PersonFactory() reg['email'] = person.email().address @@ -841,16 +843,22 @@ def test_api_new_meeting_registration_nomcom_volunteer(self): # create appropriate group and nomcom objects nomcom = NomComFactory.create(is_accepting_volunteers=True, **nomcom_kwargs_for_year(year)) url = urlreverse('ietf.api.views.api_new_meeting_registration') - r = self.client.post(url, reg) - self.assertContains(r, 'Invalid apikey', status_code=403) oidcp = PersonFactory(user__is_staff=True) # Make sure 'oidcp' has an acceptable role RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat') key = PersonalApiKey.objects.create(person=oidcp, endpoint=url) reg['apikey'] = key.hash() + + # first test is_nomcom_volunteer False r = self.client.post(url, reg) - nomcom = NomCom.objects.last() self.assertContains(r, "Accepted, New registration", status_code=202) + # assert no Volunteers exists + self.assertEqual(Volunteer.objects.count(), 0) + + # test is_nomcom_volunteer True + reg['is_nomcom_volunteer'] = 'True' + r = self.client.post(url, reg) + self.assertContains(r, "Accepted, Updated registration", status_code=202) # assert Volunteer exists self.assertEqual(Volunteer.objects.count(), 1) volunteer = Volunteer.objects.last() @@ -901,7 +909,7 @@ def test_api_get_session_matherials_no_agenda_meeting_url(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) - @override_settings(APP_API_TOKENS={"ietf.api.views.email_aliases": ["valid-token"]}) + @override_settings(APP_API_TOKENS={"ietf.api.views.draft_aliases": ["valid-token"]}) @mock.patch("ietf.api.views.DraftAliasGenerator") def test_draft_aliases(self, mock): mock.return_value = (("alias1", ("a1", "a2")), ("alias2", ("a3", "a4"))) @@ -935,7 +943,7 @@ def test_draft_aliases(self, mock): 405, ) - @override_settings(APP_API_TOKENS={"ietf.api.views.email_aliases": ["valid-token"]}) + @override_settings(APP_API_TOKENS={"ietf.api.views.group_aliases": ["valid-token"]}) @mock.patch("ietf.api.views.GroupAliasGenerator") def test_group_aliases(self, mock): mock.return_value = (("alias1", ("ietf",), ("a1", "a2")), ("alias2", ("ietf", "iab"), ("a3", "a4"))) @@ -991,6 +999,262 @@ def test_active_email_list(self): self.assertCountEqual(result.keys(), ["addresses"]) self.assertCountEqual(result["addresses"], Email.objects.filter(active=True).values_list("address", flat=True)) + @override_settings(APP_API_TOKENS={"ietf.api.views.role_holder_addresses": ["valid-token"]}) + def test_role_holder_addresses(self): + url = urlreverse("ietf.api.views.role_holder_addresses") + r = self.client.get(url, headers={}) + self.assertEqual(r.status_code, 403, "No api token, no access") + r = self.client.get(url, headers={"X-Api-Key": "not-valid-token"}) + self.assertEqual(r.status_code, 403, "Bad api token, no access") + r = self.client.post(url, headers={"X-Api-Key": "valid-token"}) + self.assertEqual(r.status_code, 405, "Bad method, no access") + + emails = EmailFactory.create_batch(5) + email_queryset = Email.objects.filter(pk__in=[e.pk for e in emails]) + with mock.patch("ietf.api.views.role_holder_emails", return_value=email_queryset): + r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) + self.assertEqual(r.status_code, 200, "Good api token and method, access") + content_dict = json.loads(r.content) + self.assertCountEqual(content_dict.keys(), ["addresses"]) + self.assertEqual( + content_dict["addresses"], + sorted(e.address for e in emails), + ) + + @override_settings(APP_API_TOKENS={"ietf.api.views.ingest_email": "valid-token"}) + @mock.patch("ietf.api.views.iana_ingest_review_email") + @mock.patch("ietf.api.views.ipr_ingest_response_email") + @mock.patch("ietf.api.views.nomcom_ingest_feedback_email") + def test_ingest_email( + self, mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest + ): + mocks = {mock_nomcom_ingest, mock_ipr_ingest, mock_iana_ingest} + empty_outbox() + url = urlreverse("ietf.api.views.ingest_email") + + # test various bad calls + r = self.client.get(url) + self.assertEqual(r.status_code, 403) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.post(url) + self.assertEqual(r.status_code, 403) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.get(url, headers={"X-Api-Key": "valid-token"}) + self.assertEqual(r.status_code, 405) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.post(url, headers={"X-Api-Key": "valid-token"}) + self.assertEqual(r.status_code, 415) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.post( + url, content_type="application/json", headers={"X-Api-Key": "valid-token"} + ) + self.assertEqual(r.status_code, 400) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.post( + url, + "this is not JSON!", + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 400) + self.assertFalse(any(m.called for m in mocks)) + + r = self.client.post( + url, + {"json": "yes", "valid_schema": False}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 400) + self.assertFalse(any(m.called for m in mocks)) + + # bad destination + message_b64 = base64.b64encode(b"This is a message").decode() + r = self.client.post( + url, + {"dest": "not-a-destination", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_dest"}) + self.assertFalse(any(m.called for m in mocks)) + + # test that valid requests call handlers appropriately + r = self.client.post( + url, + {"dest": "iana-review", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "ok"}) + self.assertTrue(mock_iana_ingest.called) + self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) + mock_iana_ingest.reset_mock() + + r = self.client.post( + url, + {"dest": "ipr-response", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "ok"}) + self.assertTrue(mock_ipr_ingest.called) + self.assertEqual(mock_ipr_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_ipr_ingest}))) + mock_ipr_ingest.reset_mock() + + # bad nomcom-feedback dest + for bad_nomcom_dest in [ + "nomcom-feedback", # no suffix + "nomcom-feedback-", # no year + "nomcom-feedback-squid", # not a year, + "nomcom-feedback-2024-2025", # also not a year + ]: + r = self.client.post( + url, + {"dest": bad_nomcom_dest, "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_dest"}) + self.assertFalse(any(m.called for m in mocks)) + + # good nomcom-feedback dest + random_year = randrange(100000) + r = self.client.post( + url, + {"dest": f"nomcom-feedback-{random_year}", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "ok"}) + self.assertTrue(mock_nomcom_ingest.called) + self.assertEqual(mock_nomcom_ingest.call_args, mock.call(b"This is a message", random_year)) + self.assertFalse(any(m.called for m in (mocks - {mock_nomcom_ingest}))) + mock_nomcom_ingest.reset_mock() + + # test that exceptions lead to email being sent - assumes that iana-review handling is representative + mock_iana_ingest.side_effect = EmailIngestionError("Error: don't send email") + r = self.client.post( + url, + {"dest": "iana-review", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_msg"}) + self.assertTrue(mock_iana_ingest.called) + self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) + self.assertEqual(len(outbox), 0) # implicitly tests that _none_ of the earlier tests sent email + mock_iana_ingest.reset_mock() + + # test default recipients and attached original message + mock_iana_ingest.side_effect = EmailIngestionError( + "Error: do send email", + email_body="This is my email\n", + email_original_message=b"This is the original message" + ) + with override_settings(ADMINS=[("Some Admin", "admin@example.com")]): + r = self.client.post( + url, + {"dest": "iana-review", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_msg"}) + self.assertTrue(mock_iana_ingest.called) + self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) + self.assertEqual(len(outbox), 1) + self.assertIn("admin@example.com", outbox[0]["To"]) + self.assertEqual("Error: do send email", outbox[0]["Subject"]) + self.assertEqual("This is my email\n", get_payload_text(outbox[0].get_body())) + attachments = list(a for a in outbox[0].iter_attachments()) + self.assertEqual(len(attachments), 1) + self.assertEqual(attachments[0].get_filename(), "original-message") + self.assertEqual(attachments[0].get_content_type(), "application/octet-stream") + self.assertEqual(attachments[0].get_content(), b"This is the original message") + mock_iana_ingest.reset_mock() + empty_outbox() + + # test overridden recipients and no attached original message + mock_iana_ingest.side_effect = EmailIngestionError( + "Error: do send email", + email_body="This is my email\n", + email_recipients=("thatguy@example.com") + ) + with override_settings(ADMINS=[("Some Admin", "admin@example.com")]): + r = self.client.post( + url, + {"dest": "iana-review", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_msg"}) + self.assertTrue(mock_iana_ingest.called) + self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) + self.assertEqual(len(outbox), 1) + self.assertNotIn("admin@example.com", outbox[0]["To"]) + self.assertIn("thatguy@example.com", outbox[0]["To"]) + self.assertEqual("Error: do send email", outbox[0]["Subject"]) + self.assertEqual("This is my email\n", get_payload_text(outbox[0])) + mock_iana_ingest.reset_mock() + empty_outbox() + + # test attached traceback + mock_iana_ingest.side_effect = EmailIngestionError( + "Error: do send email", + email_body="This is my email\n", + email_attach_traceback=True, + ) + with override_settings(ADMINS=[("Some Admin", "admin@example.com")]): + r = self.client.post( + url, + {"dest": "iana-review", "message": message_b64}, + content_type="application/json", + headers={"X-Api-Key": "valid-token"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "application/json") + self.assertEqual(json.loads(r.content), {"result": "bad_msg"}) + self.assertTrue(mock_iana_ingest.called) + self.assertEqual(mock_iana_ingest.call_args, mock.call(b"This is a message")) + self.assertFalse(any(m.called for m in (mocks - {mock_iana_ingest}))) + self.assertEqual(len(outbox), 1) + self.assertIn("admin@example.com", outbox[0]["To"]) + self.assertEqual("Error: do send email", outbox[0]["Subject"]) + self.assertEqual("This is my email\n", get_payload_text(outbox[0].get_body())) + attachments = list(a for a in outbox[0].iter_attachments()) + self.assertEqual(len(attachments), 1) + self.assertEqual(attachments[0].get_filename(), "traceback.txt") + self.assertEqual(attachments[0].get_content_type(), "text/plain") + self.assertIn("ietf.api.views.EmailIngestionError: Error: do send email", attachments[0].get_content()) + mock_iana_ingest.reset_mock() + empty_outbox() + class DirectAuthApiTests(TestCase): diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 1adc02a038..fb2184a3f0 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -24,10 +24,14 @@ # --- Custom API endpoints, sorted alphabetically --- # Email alias information for drafts url(r'^doc/draft-aliases/$', api_views.draft_aliases), - # GPRD: export of personal information for the logged-in person + # email ingestor + url(r'email/$', api_views.ingest_email), + # GDPR: export of personal information for the logged-in person url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()), # Email alias information for groups url(r'^group/group-aliases/$', api_views.group_aliases), + # Email addresses belonging to role holders + url(r'^group/role-holder-addresses/$', api_views.role_holder_addresses), # Let IESG members set positions programmatically url(r'^iesg/position', views_ballot.api_set_position), # Let Meetecho set session video URLs diff --git a/ietf/api/views.py b/ietf/api/views.py index 744c6548ae..6aaed4b6a9 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -1,10 +1,14 @@ # Copyright The IETF Trust 2017-2020, All Rights Reserved # -*- coding: utf-8 -*- +import base64 +import binascii import json +import jsonschema +import pytz import re -import pytz +from contextlib import suppress from django.conf import settings from django.contrib.auth import authenticate from django.contrib.auth.decorators import login_required @@ -18,26 +22,34 @@ from django.views.decorators.csrf import csrf_exempt from django.views.decorators.gzip import gzip_page from django.views.generic.detail import DetailView +from email.message import EmailMessage from jwcrypto.jwk import JWK from tastypie.exceptions import BadRequest from tastypie.serializers import Serializer from tastypie.utils import is_valid_jsonp_callback_value from tastypie.utils.mime import determine_format, build_content_type +from textwrap import dedent +from traceback import format_exception, extract_tb +from typing import Iterable, Optional import ietf from ietf.api import _api_list from ietf.api.ietf_utils import is_valid_token, requires_api_token from ietf.api.serializer import JsonExportMixin from ietf.doc.utils import DraftAliasGenerator, fuzzy_find_documents -from ietf.group.utils import GroupAliasGenerator +from ietf.group.utils import GroupAliasGenerator, role_holder_emails from ietf.ietfauth.utils import role_required from ietf.ietfauth.views import send_account_creation_email +from ietf.ipr.utils import ingest_response_email as ipr_ingest_response_email from ietf.meeting.models import Meeting from ietf.nomcom.models import Volunteer, NomCom +from ietf.nomcom.utils import ingest_feedback_email as nomcom_ingest_feedback_email from ietf.person.models import Person, Email from ietf.stats.models import MeetingRegistration +from ietf.sync.iana import ingest_review_email as iana_ingest_review_email from ietf.utils import log from ietf.utils.decorators import require_api_key +from ietf.utils.mail import send_smtp from ietf.utils.models import DumpInfo @@ -201,7 +213,7 @@ def err(code, text): response += ", Email sent" # handle nomcom volunteer - if data['is_nomcom_volunteer'] and object.person: + if request.POST.get('is_nomcom_volunteer', 'false').lower() == 'true' and object.person: try: nomcom = NomCom.objects.get(is_accepting_volunteers=True) except (NomCom.DoesNotExist, NomCom.MultipleObjectsReturned): @@ -452,7 +464,7 @@ def directauth(request): return HttpResponse(status=405) -@requires_api_token("ietf.api.views.email_aliases") +@requires_api_token @csrf_exempt def draft_aliases(request): if request.method == "GET": @@ -471,7 +483,7 @@ def draft_aliases(request): return HttpResponse(status=405) -@requires_api_token("ietf.api.views.email_aliases") +@requires_api_token @csrf_exempt def group_aliases(request): if request.method == "GET": @@ -500,3 +512,163 @@ def active_email_list(request): } ) return HttpResponse(status=405) + + +@requires_api_token +def role_holder_addresses(request): + if request.method == "GET": + return JsonResponse( + { + "addresses": list( + role_holder_emails() + .order_by("address") + .values_list("address", flat=True) + ) + } + ) + return HttpResponse(status=405) + + +_response_email_json_validator = jsonschema.Draft202012Validator( + schema={ + "type": "object", + "properties": { + "dest": { + "type": "string", + }, + "message": { + "type": "string", # base64-encoded mail message + }, + }, + "required": ["dest", "message"], + } +) + + +class EmailIngestionError(Exception): + """Exception indicating ingestion failed""" + def __init__( + self, + msg="Message rejected", + *, + email_body: Optional[str] = None, + email_recipients: Optional[Iterable[str]] = None, + email_attach_traceback=False, + email_original_message: Optional[bytes]=None, + ): + self.msg = msg + self.email_body = email_body + self.email_subject = msg + self.email_recipients = email_recipients + self.email_attach_traceback = email_attach_traceback + self.email_original_message = email_original_message + self.email_from = settings.SERVER_EMAIL + + @staticmethod + def _summarize_error(error): + frame = extract_tb(error.__traceback__)[-1] + return dedent(f"""\ + Error details: + Exception type: {type(error).__module__}.{type(error).__name__} + File: {frame.filename} + Line: {frame.lineno}""") + + def as_emailmessage(self) -> Optional[EmailMessage]: + """Generate an EmailMessage to report an error""" + if self.email_body is None: + return None + error = self if self.__cause__ is None else self.__cause__ + format_values = dict( + error=error, + error_summary=self._summarize_error(error), + ) + msg = EmailMessage() + if self.email_recipients is None: + msg["To"] = tuple(adm[1] for adm in settings.ADMINS) + else: + msg["To"] = self.email_recipients + msg["From"] = self.email_from + msg["Subject"] = self.msg + msg.set_content( + self.email_body.format(**format_values) + ) + if self.email_attach_traceback: + msg.add_attachment( + "".join(format_exception(None, error, error.__traceback__)), + filename="traceback.txt", + ) + if self.email_original_message is not None: + # Attach incoming message if it was provided. Send as a generic media + # type because we don't know for sure that it was actually a valid + # message. + msg.add_attachment( + self.email_original_message, + 'application', 'octet-stream', # media type + filename='original-message', + ) + return msg + + +@requires_api_token +@csrf_exempt +def ingest_email(request): + """Ingest incoming email + + Returns a 4xx or 5xx status code if the HTTP request was invalid or something went + wrong while processing it. If the request was valid, returns a 200. This may or may + not indicate that the message was accepted. + """ + + def _http_err(code, text): + return HttpResponse(text, status=code, content_type="text/plain") + + def _api_response(result): + return JsonResponse(data={"result": result}) + + if request.method != "POST": + return _http_err(405, "Method not allowed") + + if request.content_type != "application/json": + return _http_err(415, "Content-Type must be application/json") + + # Validate + try: + payload = json.loads(request.body) + _response_email_json_validator.validate(payload) + except json.decoder.JSONDecodeError as err: + return _http_err(400, f"JSON parse error at line {err.lineno} col {err.colno}: {err.msg}") + except jsonschema.exceptions.ValidationError as err: + return _http_err(400, f"JSON schema error at {err.json_path}: {err.message}") + except Exception: + return _http_err(400, "Invalid request format") + + try: + message = base64.b64decode(payload["message"], validate=True) + except binascii.Error: + return _http_err(400, "Invalid message: bad base64 encoding") + + dest = payload["dest"] + valid_dest = False + try: + if dest == "iana-review": + valid_dest = True + iana_ingest_review_email(message) + elif dest == "ipr-response": + valid_dest = True + ipr_ingest_response_email(message) + elif dest.startswith("nomcom-feedback-"): + maybe_year = dest[len("nomcom-feedback-"):] + if maybe_year.isdecimal(): + valid_dest = True + nomcom_ingest_feedback_email(message, int(maybe_year)) + except EmailIngestionError as err: + error_email = err.as_emailmessage() + if error_email is not None: + with suppress(Exception): # send_smtp logs its own exceptions, ignore them here + send_smtp(error_email) + return _api_response("bad_msg") + + if not valid_dest: + return _api_response("bad_dest") + + return _api_response("ok") diff --git a/ietf/bin/aliases-from-json.py b/ietf/bin/aliases-from-json.py index 72fcb469f7..a0c383a1ac 100644 --- a/ietf/bin/aliases-from-json.py +++ b/ietf/bin/aliases-from-json.py @@ -61,6 +61,13 @@ def generate_files(records, adest, vdest, postconfirm, vdomain): shutil.move(vpath, vdest) +def directory_path(val): + p = Path(val) + if p.is_dir(): + return p + else: + raise argparse.ArgumentTypeError(f"{p} is not a directory") + if __name__ == "__main__": parser = argparse.ArgumentParser( description="Convert a JSON stream of draft alias definitions into alias / virtual alias files." @@ -73,7 +80,7 @@ def generate_files(records, adest, vdest, postconfirm, vdomain): parser.add_argument( "--output-dir", default="./", - type=Path, + type=directory_path, help="Destination for output files.", ) parser.add_argument( @@ -87,8 +94,6 @@ def generate_files(records, adest, vdest, postconfirm, vdomain): help=f"Virtual domain (defaults to {VDOMAIN}_", ) args = parser.parse_args() - if not args.output_dir.is_dir(): - sys.stderr.write("Error: output-dir must be a directory") data = json.load(sys.stdin) generate_files( data["aliases"], diff --git a/ietf/bin/expire-last-calls b/ietf/bin/expire-last-calls deleted file mode 100755 index 83b565e192..0000000000 --- a/ietf/bin/expire-last-calls +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python - -# This script requires that the proper virtual python environment has been -# invoked before start - -import os -import sys -import syslog - -# boilerplate -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) -sys.path = [ basedir ] + sys.path -os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings" - -virtualenv_activation = os.path.join(basedir, "env", "bin", "activate_this.py") -if os.path.exists(virtualenv_activation): - execfile(virtualenv_activation, dict(__file__=virtualenv_activation)) - -syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER) - -import django -django.setup() - -# ---------------------------------------------------------------------- - -from ietf.doc.lastcall import get_expired_last_calls, expire_last_call - -drafts = get_expired_last_calls() -for doc in drafts: - try: - expire_last_call(doc) - syslog.syslog("Expired last call for %s (id=%s)" % (doc.file_tag(), doc.pk)) - except Exception as e: - syslog.syslog(syslog.LOG_ERR, "ERROR: Failed to expire last call for %s (id=%s)" % (doc.file_tag(), doc.pk)) diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates deleted file mode 100755 index c3e8f1f462..0000000000 --- a/ietf/bin/rfc-editor-index-updates +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python - -# This script requires that the proper virtual python environment has been -# invoked before start - -import datetime -import io -import os -import requests -import sys -import syslog -import traceback - -# boilerplate -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) -sys.path = [ basedir ] + sys.path -os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings" - -# Before invoking django -syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_USER) - -import django -django.setup() - -from django.conf import settings -from optparse import OptionParser -from django.core.mail import mail_admins - -from ietf.doc.utils import rebuild_reference_relations -from ietf.utils.log import log -from ietf.utils.pipe import pipe -from ietf.utils.timezone import date_today - -import ietf.sync.rfceditor - - -parser = OptionParser() -parser.add_option("-d", dest="skip_date", - help="To speed up processing skip RFCs published before this date (default is one year ago)", metavar="YYYY-MM-DD") - -options, args = parser.parse_args() - -skip_date = date_today() - datetime.timedelta(days=365) -if options.skip_date: - skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date() - -log("Updating document metadata from RFC index going back to %s, from %s" % (skip_date, settings.RFC_EDITOR_INDEX_URL)) - - -try: - response = requests.get( - settings.RFC_EDITOR_INDEX_URL, - timeout=30, # seconds - ) -except requests.Timeout as exc: - log(f'GET request timed out retrieving RFC editor index: {exc}') - sys.exit(1) - - -rfc_index_xml = response.text -index_data = ietf.sync.rfceditor.parse_index(io.StringIO(rfc_index_xml)) - -try: - response = requests.get( - settings.RFC_EDITOR_ERRATA_JSON_URL, - timeout=30, # seconds - ) -except requests.Timeout as exc: - log(f'GET request timed out retrieving RFC editor errata: {exc}') - sys.exit(1) -errata_data = response.json() - -if len(index_data) < ietf.sync.rfceditor.MIN_INDEX_RESULTS: - log("Not enough index entries, only %s" % len(index_data)) - sys.exit(1) - -if len(errata_data) < ietf.sync.rfceditor.MIN_ERRATA_RESULTS: - log("Not enough errata entries, only %s" % len(errata_data)) - sys.exit(1) - -new_rfcs = [] -for rfc_number, changes, doc, rfc_published in ietf.sync.rfceditor.update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=skip_date): - if rfc_published: - new_rfcs.append(doc) - - for c in changes: - log("RFC%s, %s: %s" % (rfc_number, doc.name, c)) - -sys.exit(0) - -# This can be called while processing a notifying POST from the RFC Editor -# Spawn a child to sync the rfcs and calculate new reference relationships -# so that the POST - -newpid = os.fork() - -if newpid == 0: - try: - pipe("%s -a %s %s" % (settings.RSYNC_BINARY,settings.RFC_TEXT_RSYNC_SOURCE,settings.RFC_PATH)) - for rfc in new_rfcs: - rebuild_reference_relations(rfc) - log("Updated references for %s"%rfc.name) - except: - subject = "Exception in updating references for new rfcs: %s : %s" % (sys.exc_info()[0],sys.exc_info()[1]) - msg = "%s\n%s\n----\n%s"%(sys.exc_info()[0],sys.exc_info()[1],traceback.format_tb(sys.exc_info()[2])) - mail_admins(subject,msg,fail_silently=True) - log(subject) - os._exit(0) -else: - sys.exit(0) diff --git a/ietf/bin/rfc-editor-queue-updates b/ietf/bin/rfc-editor-queue-updates deleted file mode 100755 index b441e50ebc..0000000000 --- a/ietf/bin/rfc-editor-queue-updates +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python - -import io -import os -import requests -import sys - -# boilerplate -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) -sys.path = [ basedir ] + sys.path -os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings" - -import django -django.setup() - -from django.conf import settings - -from ietf.sync.rfceditor import parse_queue, MIN_QUEUE_RESULTS, update_drafts_from_queue -from ietf.utils.log import log - -log("Updating RFC Editor queue states from %s" % settings.RFC_EDITOR_QUEUE_URL) - -try: - response = requests.get( - settings.RFC_EDITOR_QUEUE_URL, - timeout=30, # seconds - ) -except requests.Timeout as exc: - log(f'GET request timed out retrieving RFC editor queue: {exc}') - sys.exit(1) -drafts, warnings = parse_queue(io.StringIO(response.text)) -for w in warnings: - log(u"Warning: %s" % w) - -if len(drafts) < MIN_QUEUE_RESULTS: - log("Not enough results, only %s" % len(drafts)) - sys.exit(1) - -changed, warnings = update_drafts_from_queue(drafts) -for w in warnings: - log(u"Warning: %s" % w) - -for c in changed: - log(u"Updated %s" % c) diff --git a/ietf/celeryapp.py b/ietf/celeryapp.py index b36f134636..fda89c30be 100644 --- a/ietf/celeryapp.py +++ b/ietf/celeryapp.py @@ -1,14 +1,20 @@ import os import scout_apm.celery -from celery import Celery +import celery from scout_apm.api import Config +# Disable celery's internal logging configuration, we set it up via Django +@celery.signals.setup_logging.connect +def on_setup_logging(**kwargs): + pass + + # Set the default Django settings module for the 'celery' program os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ietf.settings') -app = Celery('ietf') +app = celery.Celery('ietf') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. @@ -17,10 +23,13 @@ app.config_from_object('django.conf:settings', namespace='CELERY') # Turn on Scout APM celery instrumentation if configured in the environment -scout_key = os.environ.get("SCOUT_KEY", "") -scout_name = os.environ.get("SCOUT_NAME", "") -scout_core_agent_socket_path = os.environ.get("SCOUT_CORE_AGENT_SOCKET_PATH", "tcp://scoutapm:6590") -if scout_key and scout_name: +scout_key = os.environ.get("DATATRACKER_SCOUT_KEY", None) +if scout_key is not None: + scout_name = os.environ.get("DATATRACKER_SCOUT_NAME", "Datatracker") + scout_core_agent_socket_path = "tcp://{host}:{port}".format( + host=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_HOST", "localhost"), + port=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_PORT", "6590"), + ) Config.set( key=scout_key, name=scout_name, diff --git a/ietf/checks.py b/ietf/checks.py index f9ad87db70..f911d081f0 100644 --- a/ietf/checks.py +++ b/ietf/checks.py @@ -28,61 +28,6 @@ def already_ran(): checks_run.append(name) return False - -@checks.register('files') -def check_group_email_aliases_exists(app_configs, **kwargs): - from ietf.group.views import check_group_email_aliases - # - if already_ran(): - return [] - # - errors = [] - try: - ok = check_group_email_aliases() - if not ok: - errors.append(checks.Error( - "Found no aliases in the group email aliases file\n'%s'."%settings.GROUP_ALIASES_PATH, - hint="Please run the generate_group_aliases management command to generate them.", - obj=None, - id="datatracker.E0002", - )) - except IOError as e: - errors.append(checks.Error( - "Could not read group email aliases:\n %s" % e, - hint="Please run the generate_group_aliases management command to generate them.", - obj=None, - id="datatracker.E0003", - )) - - return errors - -@checks.register('files') -def check_doc_email_aliases_exists(app_configs, **kwargs): - from ietf.doc.views_doc import check_doc_email_aliases - # - if already_ran(): - return [] - # - errors = [] - try: - ok = check_doc_email_aliases() - if not ok: - errors.append(checks.Error( - "Found no aliases in the document email aliases file\n'%s'."%settings.DRAFT_VIRTUAL_PATH, - hint="Please run the generate_draft_aliases management command to generate them.", - obj=None, - id="datatracker.E0004", - )) - except IOError as e: - errors.append(checks.Error( - "Could not read document email aliases:\n %s" % e, - hint="Please run the generate_draft_aliases management command to generate them.", - obj=None, - id="datatracker.E0005", - )) - - return errors - @checks.register('directories') def check_id_submission_directories(app_configs, **kwargs): # diff --git a/ietf/community/models.py b/ietf/community/models.py index 2e40031768..b1295461d6 100644 --- a/ietf/community/models.py +++ b/ietf/community/models.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- +from django.conf import settings from django.db import models from django.db.models import signals from django.urls import reverse as urlreverse @@ -11,6 +12,9 @@ from ietf.person.models import Person, Email from ietf.utils.models import ForeignKey +from .tasks import notify_event_to_subscribers_task + + class CommunityList(models.Model): person = ForeignKey(Person, blank=True, null=True) group = ForeignKey(Group, blank=True, null=True) @@ -106,8 +110,11 @@ def notify_events(sender, instance, **kwargs): if getattr(instance, "skip_community_list_notification", False): return - from ietf.community.utils import notify_event_to_subscribers - notify_event_to_subscribers(instance) + # kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to + # start a Celery task during tests. To prevent this, don't queue a celery task if we're running + # tests. + if settings.SERVER_MODE != "test": + notify_event_to_subscribers_task.delay(event_id=instance.pk) signals.post_save.connect(notify_events) diff --git a/ietf/community/tasks.py b/ietf/community/tasks.py new file mode 100644 index 0000000000..763a596495 --- /dev/null +++ b/ietf/community/tasks.py @@ -0,0 +1,15 @@ +# Copyright The IETF Trust 2024, All Rights Reserved +from celery import shared_task + +from ietf.doc.models import DocEvent +from ietf.utils.log import log + + +@shared_task +def notify_event_to_subscribers_task(event_id): + from .utils import notify_event_to_subscribers + event = DocEvent.objects.filter(pk=event_id).first() + if event is None: + log(f"Unable to send subscriber notifications because DocEvent {event_id} was not found") + else: + notify_event_to_subscribers(event) diff --git a/ietf/community/tests.py b/ietf/community/tests.py index 387877887f..d76347b70a 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -2,15 +2,18 @@ # -*- coding: utf-8 -*- +import mock from pyquery import PyQuery +from django.test.utils import override_settings from django.urls import reverse as urlreverse import debug # pyflakes:ignore from ietf.community.models import CommunityList, SearchRule, EmailSubscription from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc -from ietf.community.utils import reset_name_contains_index_for_rule +from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers +from ietf.community.tasks import notify_event_to_subscribers_task import ietf.community.views from ietf.group.models import Group from ietf.group.utils import setup_default_community_list_for_group @@ -18,8 +21,7 @@ from ietf.doc.utils import add_state_change_event from ietf.person.models import Person, Email, Alias from ietf.utils.test_utils import TestCase, login_testing_unauthorized -from ietf.utils.mail import outbox -from ietf.doc.factories import WgDraftFactory +from ietf.doc.factories import DocEventFactory, WgDraftFactory from ietf.group.factories import GroupFactory, RoleFactory from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory @@ -423,7 +425,49 @@ def test_subscription_for_group(self): r = self.client.get(url) self.assertEqual(r.status_code, 200) - def test_notification(self): + @mock.patch("ietf.community.models.notify_event_to_subscribers_task") + def test_notification_signal_receiver(self, mock_notify_task): + """Saving a DocEvent should notify subscribers + + This implicitly tests that notify_events is hooked up to the post_save signal. + """ + # Arbitrary model that's not a DocEvent + p = PersonFactory() + mock_notify_task.reset_mock() # clear any calls that resulted from the factories + # be careful overriding SERVER_MODE - we do it here because the method + # under test does not make this call when in "test" mode + with override_settings(SERVER_MODE="not-test"): + p.save() + self.assertFalse(mock_notify_task.delay.called) + + d = DocEventFactory() + mock_notify_task.reset_mock() # clear any calls that resulted from the factories + # be careful overriding SERVER_MODE - we do it here because the method + # under test does not make this call when in "test" mode + with override_settings(SERVER_MODE="not-test"): + d.save() + self.assertEqual(mock_notify_task.delay.call_count, 1) + self.assertEqual(mock_notify_task.delay.call_args, mock.call(event_id = d.pk)) + + mock_notify_task.reset_mock() + d.skip_community_list_notification = True + # be careful overriding SERVER_MODE - we do it here because the method + # under test does not make this call when in "test" mode + with override_settings(SERVER_MODE="not-test"): + d.save() + self.assertFalse(mock_notify_task.delay.called) + + del(d.skip_community_list_notification) + d.doc.type_id="rfc" # not "draft" + d.doc.save() + # be careful overriding SERVER_MODE - we do it here because the method + # under test does not make this call when in "test" mode + with override_settings(SERVER_MODE="not-test"): + d.save() + self.assertFalse(mock_notify_task.delay.called) + + @mock.patch("ietf.utils.mail.send_mail_text") + def test_notify_event_to_subscribers(self, mock_send_mail_text): person = PersonFactory(user__username='plain') draft = WgDraftFactory() @@ -431,18 +475,55 @@ def test_notification(self): if not draft in clist.added_docs.all(): clist.added_docs.add(draft) - EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant") + sub_to_significant = EmailSubscription.objects.create( + community_list=clist, + email=Email.objects.filter(person__user__username="plain").first(), + notify_on="significant", + ) + sub_to_all = EmailSubscription.objects.create( + community_list=clist, + email=Email.objects.filter(person__user__username="plain").first(), + notify_on="all", + ) - mailbox_before = len(outbox) active_state = State.objects.get(type="draft", slug="active") system = Person.objects.get(name="(System)") - add_state_change_event(draft, system, None, active_state) - self.assertEqual(len(outbox), mailbox_before) + event = add_state_change_event(draft, system, None, active_state) + notify_event_to_subscribers(event) + self.assertEqual(mock_send_mail_text.call_count, 1) + address = mock_send_mail_text.call_args[0][1] + subject = mock_send_mail_text.call_args[0][3] + content = mock_send_mail_text.call_args[0][4] + self.assertEqual(address, sub_to_all.email.address) + self.assertIn(draft.name, subject) + self.assertIn(clist.long_name(), content) - mailbox_before = len(outbox) rfc_state = State.objects.get(type="draft", slug="rfc") - add_state_change_event(draft, system, active_state, rfc_state) - self.assertEqual(len(outbox), mailbox_before + 1) - self.assertTrue(draft.name in outbox[-1]["Subject"]) - - + event = add_state_change_event(draft, system, active_state, rfc_state) + mock_send_mail_text.reset_mock() + notify_event_to_subscribers(event) + self.assertEqual(mock_send_mail_text.call_count, 2) + addresses = [call_args[0][1] for call_args in mock_send_mail_text.call_args_list] + subjects = {call_args[0][3] for call_args in mock_send_mail_text.call_args_list} + contents = {call_args[0][4] for call_args in mock_send_mail_text.call_args_list} + self.assertCountEqual( + addresses, + [sub_to_significant.email.address, sub_to_all.email.address], + ) + self.assertEqual(len(subjects), 1) + self.assertIn(draft.name, subjects.pop()) + self.assertEqual(len(contents), 1) + self.assertIn(clist.long_name(), contents.pop()) + + @mock.patch("ietf.community.utils.notify_event_to_subscribers") + def test_notify_event_to_subscribers_task(self, mock_notify): + d = DocEventFactory() + notify_event_to_subscribers_task(event_id=d.pk) + self.assertEqual(mock_notify.call_count, 1) + self.assertEqual(mock_notify.call_args, mock.call(d)) + mock_notify.reset_mock() + + d.delete() + notify_event_to_subscribers_task(event_id=d.pk) + self.assertFalse(mock_notify.called) + diff --git a/ietf/doc/expire.py b/ietf/doc/expire.py index f6779e0471..1650b4ddf5 100644 --- a/ietf/doc/expire.py +++ b/ietf/doc/expire.py @@ -139,6 +139,9 @@ def move_file(f): if os.path.exists(src): try: + # ghostlinkd would keep this in the combined all archive since it would + # be sourced from a different place. But when ghostlinkd is removed, nothing + # new is needed here - the file will already exist in the combined archive shutil.move(src, dst) except IOError as e: if "No such file or directory" in str(e): @@ -213,6 +216,10 @@ def splitext(fn): filename, revision = match.groups() def move_file_to(subdir): + # Similar to move_draft_files_to_archive + # ghostlinkd would keep this in the combined all archive since it would + # be sourced from a different place. But when ghostlinkd is removed, nothing + # new is needed here - the file will already exist in the combined archive shutil.move(path, os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, subdir, basename)) @@ -229,4 +236,5 @@ def move_file_to(subdir): move_file_to("") except Document.DoesNotExist: + # All uses of this past 2014 seem related to major system failures. move_file_to("unknown_ids") diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py index 554451c564..f77b218318 100644 --- a/ietf/doc/forms.py +++ b/ietf/doc/forms.py @@ -266,3 +266,24 @@ def clean(self): @staticmethod def valid_resource_tags(): return ExtResourceName.objects.all().order_by('slug').values_list('slug', flat=True) + +class InvestigateForm(forms.Form): + name_fragment = forms.CharField( + label="File name or fragment to investigate", + required=True, + help_text=( + "Enter a filename such as draft-ietf-some-draft-00.txt or a fragment like draft-ietf-some-draft using at least 8 characters. The search will also work for files that are not necessarily drafts." + ), + min_length=8, + ) + + def clean_name_fragment(self): + disallowed_characters = ["%", "/", "\\", "*"] + name_fragment = self.cleaned_data["name_fragment"] + # Manual inspection of the directories at the time of this writing shows + # looking for files with less than 8 characters in the name is not useful + # Requiring this will help protect against the secretariat unintentionally + # matching every draft. + if any(c in name_fragment for c in disallowed_characters): + raise ValidationError(f"The following characters are disallowed: {', '.join(disallowed_characters)}") + return name_fragment diff --git a/ietf/doc/management/commands/generate_draft_aliases.py b/ietf/doc/management/commands/generate_draft_aliases.py deleted file mode 100755 index 6d42a66a18..0000000000 --- a/ietf/doc/management/commands/generate_draft_aliases.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright The IETF Trust 2012-2021, All Rights Reserved -# -*- coding: utf-8 -*- - -# This was written as a script by Markus Stenberg . -# It was turned into a management command by Russ Housley . - -import datetime -import io -import os -import re -import shutil -import stat -import time - -from tempfile import mkstemp - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.utils import timezone - -import debug # pyflakes:ignore - -from ietf.doc.models import Document -from ietf.group.utils import get_group_role_emails, get_group_ad_emails -from ietf.utils.aliases import dump_sublist -from utils.mail import parseaddr -from ietf.utils import log - -DEFAULT_YEARS = 2 - - -def get_draft_ad_emails(doc): - """Get AD email addresses for the given draft, if any.""" - ad_emails = set() - # If working group document, return current WG ADs - if doc.group and doc.group.acronym != 'none': - ad_emails.update(get_group_ad_emails(doc.group)) - # Document may have an explicit AD set - if doc.ad: - ad_emails.add(doc.ad.email_address()) - return ad_emails - - -def get_draft_chair_emails(doc): - """Get chair email addresses for the given draft, if any.""" - chair_emails = set() - if doc.group: - chair_emails.update(get_group_role_emails(doc.group, ['chair', 'secr'])) - return chair_emails - - -def get_draft_shepherd_email(doc): - """Get shepherd email addresses for the given draft, if any.""" - shepherd_email = set() - if doc.shepherd: - shepherd_email.add(doc.shepherd.email_address()) - return shepherd_email - - -def get_draft_authors_emails(doc): - """Get list of authors for the given draft.""" - author_emails = set() - for author in doc.documentauthor_set.all(): - if author.email and author.email.email_address(): - author_emails.add(author.email.email_address()) - return author_emails - - -def get_draft_notify_emails(doc): - """Get list of email addresses to notify for the given draft.""" - ad_email_alias_regex = r"^%s.ad@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER) - all_email_alias_regex = r"^%s.all@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER) - author_email_alias_regex = r"^%s@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER) - notify_email_alias_regex = r"^%s.notify@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER) - shepherd_email_alias_regex = r"^%s.shepherd@(%s|%s)$" % (doc.name, settings.DRAFT_ALIAS_DOMAIN, settings.TOOLS_SERVER) - notify_emails = set() - if doc.notify: - for e in doc.notify.split(','): - e = e.strip() - if re.search(ad_email_alias_regex, e): - notify_emails.update(get_draft_ad_emails(doc)) - elif re.search(author_email_alias_regex, e): - notify_emails.update(get_draft_authors_emails(doc)) - elif re.search(shepherd_email_alias_regex, e): - notify_emails.update(get_draft_shepherd_email(doc)) - elif re.search(all_email_alias_regex, e): - notify_emails.update(get_draft_ad_emails(doc)) - notify_emails.update(get_draft_authors_emails(doc)) - notify_emails.update(get_draft_shepherd_email(doc)) - elif re.search(notify_email_alias_regex, e): - pass - else: - (name, email) = parseaddr(e) - notify_emails.add(email) - return notify_emails - - -class Command(BaseCommand): - help = ('Generate the draft-aliases and draft-virtual files for Internet-Draft ' - 'mail aliases, placing them in the files configured in ' - 'settings.DRAFT_ALIASES_PATH and settings.DRAFT_VIRTUAL_PATH, ' - 'respectively. The generation includes aliases for Internet-Drafts ' - 'that have seen activity in the last %s years.' % (DEFAULT_YEARS)) - - def handle(self, *args, **options): - show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365) - - date = time.strftime("%Y-%m-%d_%H:%M:%S") - signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date) - - ahandle, aname = mkstemp() - os.close(ahandle) - afile = io.open(aname,"w") - - vhandle, vname = mkstemp() - os.close(vhandle) - vfile = io.open(vname,"w") - - afile.write(signature) - vfile.write(signature) - vfile.write("%s anything\n" % settings.DRAFT_VIRTUAL_DOMAIN) - - # Internet-Drafts with active status or expired within DEFAULT_YEARS - drafts = Document.objects.filter(type_id="draft") - active_drafts = drafts.filter(states__slug='active') - inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since) - interesting_drafts = active_drafts | inactive_recent_drafts - - alias_domains = ['ietf.org', ] - for draft in interesting_drafts.distinct().iterator(): - # Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS - if draft.get_state_slug()=="rfc": - rfc = draft.became_rfc() - log.assertion("rfc is not None") - if rfc.latest_event(type='published_rfc').time < show_since: - continue - - alias = draft.name - all = set() - - # no suffix and .authors are the same list - emails = get_draft_authors_emails(draft) - all.update(emails) - dump_sublist(afile, vfile, alias, alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - dump_sublist(afile, vfile, alias+'.authors', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - - # .chairs = group chairs - emails = get_draft_chair_emails(draft) - if emails: - all.update(emails) - dump_sublist(afile, vfile, alias+'.chairs', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - - # .ad = sponsoring AD / WG AD (WG document) - emails = get_draft_ad_emails(draft) - if emails: - all.update(emails) - dump_sublist(afile, vfile, alias+'.ad', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - - # .notify = notify email list from the Document - emails = get_draft_notify_emails(draft) - if emails: - all.update(emails) - dump_sublist(afile, vfile, alias+'.notify', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - - # .shepherd = shepherd email from the Document - emails = get_draft_shepherd_email(draft) - if emails: - all.update(emails) - dump_sublist(afile, vfile, alias+'.shepherd', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, emails) - - # .all = everything from above - dump_sublist(afile, vfile, alias+'.all', alias_domains, settings.DRAFT_VIRTUAL_DOMAIN, all) - - afile.close() - vfile.close() - - os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH) - os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH) - - shutil.move(aname, settings.DRAFT_ALIASES_PATH) - shutil.move(vname, settings.DRAFT_VIRTUAL_PATH) - - \ No newline at end of file diff --git a/ietf/doc/management/commands/generate_draft_bibxml_files.py b/ietf/doc/management/commands/generate_draft_bibxml_files.py deleted file mode 100644 index eda67c401b..0000000000 --- a/ietf/doc/management/commands/generate_draft_bibxml_files.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright The IETF Trust 2012-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import datetime -import io -import os -import re -import sys - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.utils import timezone - -import debug # pyflakes:ignore - -from ietf.doc.models import NewRevisionDocEvent -from ietf.doc.utils import bibxml_for_draft - -DEFAULT_DAYS = 7 - -class Command(BaseCommand): - help = ('Generate draft bibxml files for xml2rfc references, placing them in the ' - 'directory configured in settings.BIBXML_BASE_PATH: %s. ' - 'By default, generate files as needed for new Internet-Draft revisions from the ' - 'last %s days.' % (settings.BIBXML_BASE_PATH, DEFAULT_DAYS)) - - def add_arguments(self, parser): - parser.add_argument('--all', action='store_true', default=False, help="Process all documents, not only recent submissions") - parser.add_argument('--days', type=int, default=DEFAULT_DAYS, help="Look submissions from the last DAYS days, instead of %s" % DEFAULT_DAYS) - - def say(self, msg): - if self.verbosity > 0: - sys.stdout.write(msg) - sys.stdout.write('\n') - - def note(self, msg): - if self.verbosity > 1: - sys.stdout.write(msg) - sys.stdout.write('\n') - - def mutter(self, msg): - if self.verbosity > 2: - sys.stdout.write(msg) - sys.stdout.write('\n') - - def write(self, fn, new): - # normalize new - new = re.sub(r'\r\n?', r'\n', new) - try: - with io.open(fn, encoding='utf-8') as f: - old = f.read() - except IOError: - old = "" - if old.strip() != new.strip(): - self.note('Writing %s' % os.path.basename(fn)) - with io.open(fn, "w", encoding='utf-8') as f: - f.write(new) - - def handle(self, *args, **options): - self.verbosity = options.get("verbosity", 1) - process_all = options.get("all") - days = options.get("days") - # - bibxmldir = os.path.join(settings.BIBXML_BASE_PATH, 'bibxml-ids') - if not os.path.exists(bibxmldir): - os.makedirs(bibxmldir) - # - if process_all: - doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft') - else: - start = timezone.now() - datetime.timedelta(days=days) - doc_events = NewRevisionDocEvent.objects.filter(type='new_revision', doc__type_id='draft', time__gte=start) - doc_events = doc_events.order_by('time') - - for e in doc_events: - self.mutter('%s %s' % (e.time, e.doc.name)) - try: - doc = e.doc - bibxml = bibxml_for_draft(doc, e.rev) - ref_rev_file_name = os.path.join(bibxmldir, 'reference.I-D.%s-%s.xml' % (doc.name, e.rev)) - self.write(ref_rev_file_name, bibxml) - except Exception as ee: - sys.stderr.write('\n%s-%s: %s\n' % (doc.name, doc.rev, ee)) diff --git a/ietf/doc/management/commands/generate_idnits2_rfc_status.py b/ietf/doc/management/commands/generate_idnits2_rfc_status.py deleted file mode 100644 index 45be188018..0000000000 --- a/ietf/doc/management/commands/generate_idnits2_rfc_status.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright The IETF Trust 2021 All Rights Reserved - -import os - -from django.conf import settings -from django.core.management.base import BaseCommand - -from ietf.doc.utils import generate_idnits2_rfc_status -from ietf.utils.log import log - -class Command(BaseCommand): - help = ('Generate the rfc_status blob used by idnits2') - - def handle(self, *args, **options): - filename=os.path.join(settings.DERIVED_DIR,'idnits2-rfc-status') - blob = generate_idnits2_rfc_status() - try: - bytes = blob.encode('utf-8') - with open(filename,'wb') as f: - f.write(bytes) - except Exception as e: - log('failed to write idnits2-rfc-status: '+str(e)) - raise e diff --git a/ietf/doc/management/commands/generate_idnits2_rfcs_obsoleted.py b/ietf/doc/management/commands/generate_idnits2_rfcs_obsoleted.py deleted file mode 100644 index 8bd122e87e..0000000000 --- a/ietf/doc/management/commands/generate_idnits2_rfcs_obsoleted.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright The IETF Trust 2021 All Rights Reserved - -import os - -from django.conf import settings -from django.core.management.base import BaseCommand - -from ietf.doc.utils import generate_idnits2_rfcs_obsoleted -from ietf.utils.log import log - -class Command(BaseCommand): - help = ('Generate the rfcs-obsoleted file used by idnits2') - - def handle(self, *args, **options): - filename=os.path.join(settings.DERIVED_DIR,'idnits2-rfcs-obsoleted') - blob = generate_idnits2_rfcs_obsoleted() - try: - bytes = blob.encode('utf-8') - with open(filename,'wb') as f: - f.write(bytes) - except Exception as e: - log('failed to write idnits2-rfcs-obsoleted: '+str(e)) - raise e diff --git a/ietf/doc/models.py b/ietf/doc/models.py index d97e8238ec..a103fca645 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -142,6 +142,7 @@ def get_file_path(self): if self.is_dochistory(): self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR else: + # This could be simplified since anything in INTERNET_DRAFT_PATH is also already in INTERNET_ALL_DRAFTS_ARCHIVE_DIR draft_state = self.get_state('draft') if draft_state and draft_state.slug == 'active': self._cached_file_path = settings.INTERNET_DRAFT_PATH diff --git a/ietf/doc/tasks.py b/ietf/doc/tasks.py index a2e83e9e26..209db035a4 100644 --- a/ietf/doc/tasks.py +++ b/ietf/doc/tasks.py @@ -6,6 +6,10 @@ import debug # pyflakes:ignore from celery import shared_task +from pathlib import Path + +from django.conf import settings +from django.utils import timezone from ietf.utils import log from ietf.utils.timezone import datetime_today @@ -20,7 +24,14 @@ get_soon_to_expire_drafts, send_expire_warning_for_draft, ) -from .models import Document +from .lastcall import get_expired_last_calls, expire_last_call +from .models import Document, NewRevisionDocEvent +from .utils import ( + generate_idnits2_rfc_status, + generate_idnits2_rfcs_obsoleted, + update_or_create_draft_bibxml_file, + ensure_draft_bibxml_path_exists, +) @shared_task @@ -54,3 +65,55 @@ def expire_ids_task(): def notify_expirations_task(notify_days=14): for doc in get_soon_to_expire_drafts(notify_days): send_expire_warning_for_draft(doc) + + +@shared_task +def expire_last_calls_task(): + for doc in get_expired_last_calls(): + try: + expire_last_call(doc) + except Exception: + log.log(f"ERROR: Failed to expire last call for {doc.file_tag()} (id={doc.pk})") + else: + log.log(f"Expired last call for {doc.file_tag()} (id={doc.pk})") + + +@shared_task +def generate_idnits2_rfc_status_task(): + outpath = Path(settings.DERIVED_DIR) / "idnits2-rfc-status" + blob = generate_idnits2_rfc_status() + try: + outpath.write_text(blob, encoding="utf8") + except Exception as e: + log.log(f"failed to write idnits2-rfc-status: {e}") + + +@shared_task +def generate_idnits2_rfcs_obsoleted_task(): + outpath = Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted" + blob = generate_idnits2_rfcs_obsoleted() + try: + outpath.write_text(blob, encoding="utf8") + except Exception as e: + log.log(f"failed to write idnits2-rfcs-obsoleted: {e}") + + +@shared_task +def generate_draft_bibxml_files_task(days=7, process_all=False): + """Generate bibxml files for recently updated docs + + If process_all is False (the default), processes only docs with new revisions + in the last specified number of days. + """ + ensure_draft_bibxml_path_exists() + doc_events = NewRevisionDocEvent.objects.filter( + type="new_revision", + doc__type_id="draft", + ).order_by("time") + if not process_all: + doc_events = doc_events.filter(time__gte=timezone.now() - datetime.timedelta(days=days)) + for event in doc_events: + try: + update_or_create_draft_bibxml_file(event.doc, event.rev) + except Exception as err: + log.log(f"Error generating bibxml for {event.doc.name}-{event.rev}: {err}") diff --git a/ietf/doc/templatetags/ietf_filters.py b/ietf/doc/templatetags/ietf_filters.py index cfed7aa1db..35f9f91b43 100644 --- a/ietf/doc/templatetags/ietf_filters.py +++ b/ietf/doc/templatetags/ietf_filters.py @@ -4,6 +4,7 @@ import datetime import re +from pathlib import Path from urllib.parse import urljoin from zoneinfo import ZoneInfo @@ -881,3 +882,79 @@ def badgeify(blob): ) return text + +@register.filter +def simple_history_delta_changes(history): + """Returns diff between given history and previous entry.""" + prev = history.prev_record + if prev: + delta = history.diff_against(prev) + return delta.changes + return [] + +@register.filter +def simple_history_delta_change_cnt(history): + """Returns number of changes between given history and previous entry.""" + prev = history.prev_record + if prev: + delta = history.diff_against(prev) + return len(delta.changes) + return 0 + +@register.filter +def mtime(path): + """Returns a datetime object representing mtime given a pathlib Path object""" + return datetime.datetime.fromtimestamp(path.stat().st_mtime).astimezone(ZoneInfo(settings.TIME_ZONE)) + +@register.filter +def mtime_is_epoch(path): + return path.stat().st_mtime == 0 + +@register.filter +def url_for_path(path): + """Consructs a 'best' URL for web access to the given pathlib Path object. + + Assumes that the path is into the Internet-Draft archive or the proceedings. + """ + if Path(settings.AGENDA_PATH) in path.parents: + return ( + f"https://www.ietf.org/proceedings/{path.relative_to(settings.AGENDA_PATH)}" + ) + elif any( + [ + pathdir in path.parents + for pathdir in [ + Path(settings.INTERNET_DRAFT_PATH), + Path(settings.INTERNET_DRAFT_ARCHIVE_DIR).parent, + Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR), + ] + ] + ): + return f"{settings.IETF_ID_ARCHIVE_URL}{path.name}" + else: + return "#" + + +@register.filter +def is_in_stream(doc): + """ + Check if the doc is in one of the states in it stream that + indicate that is actually adopted, i.e., part of the stream. + (There are various "candidate" states that necessitate this + filter.) + """ + if not doc.stream: + return False + stream = doc.stream.slug + state = doc.get_state_slug(f"draft-stream-{doc.stream.slug}") + if not state: + return True + if stream == "ietf": + return state not in ["wg-cand", "c-adopt"] + elif stream == "irtf": + return state != "candidat" + elif stream == "iab": + return state not in ["candidat", "diff-org"] + elif stream == "editorial": + return True + return False diff --git a/ietf/doc/templatetags/tests_ietf_filters.py b/ietf/doc/templatetags/tests_ietf_filters.py index 72796abeb2..f018b7d9b3 100644 --- a/ietf/doc/templatetags/tests_ietf_filters.py +++ b/ietf/doc/templatetags/tests_ietf_filters.py @@ -7,9 +7,20 @@ IndividualDraftFactory, CharterFactory, NewRevisionDocEventFactory, + StatusChangeFactory, + RgDraftFactory, + EditorialDraftFactory, + WgDraftFactory, + ConflictReviewFactory, + BofreqFactory, + StatementFactory, ) from ietf.doc.models import DocEvent -from ietf.doc.templatetags.ietf_filters import urlize_ietf_docs, is_valid_url +from ietf.doc.templatetags.ietf_filters import ( + urlize_ietf_docs, + is_valid_url, + is_in_stream, +) from ietf.person.models import Person from ietf.utils.test_utils import TestCase @@ -19,13 +30,28 @@ class IetfFiltersTests(TestCase): + def test_is_in_stream(self): + for draft in [ + IndividualDraftFactory(), + CharterFactory(), + StatusChangeFactory(), + ConflictReviewFactory(), + StatementFactory(), + BofreqFactory(), + ]: + self.assertFalse(is_in_stream(draft)) + for draft in [RgDraftFactory(), WgDraftFactory(), EditorialDraftFactory()]: + self.assertTrue(is_in_stream(draft)) + for stream in ["iab", "ietf", "irtf", "ise", "editorial"]: + self.assertTrue(is_in_stream(IndividualDraftFactory(stream_id=stream))) + def test_is_valid_url(self): cases = [(settings.IDTRACKER_BASE_URL, True), ("not valid", False)] for url, result in cases: self.assertEqual(is_valid_url(url), result) def test_urlize_ietf_docs(self): - rfc = WgRfcFactory(rfc_number=123456,std_level_id="bcp") + rfc = WgRfcFactory(rfc_number=123456, std_level_id="bcp") rfc.save_with_history( [ DocEvent.objects.create( @@ -57,7 +83,6 @@ def test_urlize_ietf_docs(self): cases = [ ("no change", "no change"), - # TODO: rework subseries when we add them # ("bCp123456", 'bCp123456'), # ("Std 00123456", 'Std 00123456'), diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 0ad26b7adc..5d0bb9c4f9 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -16,11 +16,9 @@ from pathlib import Path from pyquery import PyQuery from urllib.parse import urlparse, parse_qs -from tempfile import NamedTemporaryFile from collections import defaultdict from zoneinfo import ZoneInfo -from django.core.management import call_command from django.urls import reverse as urlreverse from django.conf import settings from django.forms import Form @@ -37,7 +35,7 @@ from ietf.doc.models import ( Document, DocRelationshipName, RelatedDocument, State, DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, BallotType, - EditedAuthorsDocEvent ) + EditedAuthorsDocEvent, StateType) from ietf.doc.factories import ( DocumentFactory, DocEventFactory, CharterFactory, ConflictReviewFactory, WgDraftFactory, IndividualDraftFactory, WgRfcFactory, IndividualRfcFactory, StateDocEventFactory, BallotPositionDocEventFactory, @@ -45,7 +43,15 @@ StatusChangeFactory, DocExtResourceFactory, RgDraftFactory, BcpFactory) from ietf.doc.forms import NotifyForm from ietf.doc.fields import SearchableDocumentsField -from ietf.doc.utils import create_ballot_if_not_open, uppercase_std_abbreviated_name, DraftAliasGenerator +from ietf.doc.utils import ( + create_ballot_if_not_open, + investigate_fragment, + uppercase_std_abbreviated_name, + DraftAliasGenerator, + generate_idnits2_rfc_status, + generate_idnits2_rfcs_obsoleted, + get_doc_email_aliases, +) from ietf.group.models import Group, Role from ietf.group.factories import GroupFactory, RoleFactory from ietf.ipr.factories import HolderIprDisclosureFactory @@ -2163,170 +2169,6 @@ def test_references(self): self.assertContains(r, doc1.name) class GenerateDraftAliasesTests(TestCase): - def setUp(self): - super().setUp() - self.doc_aliases_file = NamedTemporaryFile(delete=False, mode="w+") - self.doc_aliases_file.close() - self.doc_virtual_file = NamedTemporaryFile(delete=False, mode="w+") - self.doc_virtual_file.close() - self.saved_draft_aliases_path = settings.DRAFT_ALIASES_PATH - self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH - settings.DRAFT_ALIASES_PATH = self.doc_aliases_file.name - settings.DRAFT_VIRTUAL_PATH = self.doc_virtual_file.name - - def tearDown(self): - settings.DRAFT_ALIASES_PATH = self.saved_draft_aliases_path - settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path - os.unlink(self.doc_aliases_file.name) - os.unlink(self.doc_virtual_file.name) - super().tearDown() - - def testManagementCommand(self): - a_month_ago = (timezone.now() - datetime.timedelta(30)).astimezone(RPC_TZINFO) - a_month_ago = a_month_ago.replace(hour=0, minute=0, second=0, microsecond=0) - ad = RoleFactory( - name_id="ad", group__type_id="area", group__state_id="active" - ).person - shepherd = PersonFactory() - author1 = PersonFactory() - author2 = PersonFactory() - author3 = PersonFactory() - author4 = PersonFactory() - author5 = PersonFactory() - author6 = PersonFactory() - mars = GroupFactory(type_id="wg", acronym="mars") - marschairman = PersonFactory(user__username="marschairman") - mars.role_set.create( - name_id="chair", person=marschairman, email=marschairman.email() - ) - doc1 = IndividualDraftFactory( - authors=[author1], shepherd=shepherd.email(), ad=ad - ) - doc2 = WgDraftFactory( - name="draft-ietf-mars-test", group__acronym="mars", authors=[author2], ad=ad - ) - doc3 = WgDraftFactory.create( - name="draft-ietf-mars-finished", - group__acronym="mars", - authors=[author3], - ad=ad, - std_level_id="ps", - states=[("draft", "rfc"), ("draft-iesg", "pub")], - time=a_month_ago, - ) - rfc3 = WgRfcFactory() - DocEventFactory.create(doc=rfc3, type="published_rfc", time=a_month_ago) - doc3.relateddocument_set.create( - relationship_id="became_rfc", target=rfc3 - ) - doc4 = WgDraftFactory.create( - authors=[author4, author5], - ad=ad, - std_level_id="ps", - states=[("draft", "rfc"), ("draft-iesg", "pub")], - time=datetime.datetime(2010, 10, 10, tzinfo=ZoneInfo(settings.TIME_ZONE)), - ) - rfc4 = WgRfcFactory() - DocEventFactory.create( - doc=rfc4, - type="published_rfc", - time=datetime.datetime(2010, 10, 10, tzinfo=RPC_TZINFO), - ) - doc4.relateddocument_set.create( - relationship_id="became_rfc", target=rfc4 - ) - doc5 = IndividualDraftFactory(authors=[author6]) - - args = [] - kwargs = {} - out = io.StringIO() - call_command("generate_draft_aliases", *args, **kwargs, stdout=out, stderr=out) - self.assertFalse(out.getvalue()) - - with open(settings.DRAFT_ALIASES_PATH) as afile: - acontent = afile.read() - for x in [ - "xfilter-" + doc1.name, - "xfilter-" + doc1.name + ".ad", - "xfilter-" + doc1.name + ".authors", - "xfilter-" + doc1.name + ".shepherd", - "xfilter-" + doc1.name + ".all", - "xfilter-" + doc2.name, - "xfilter-" + doc2.name + ".ad", - "xfilter-" + doc2.name + ".authors", - "xfilter-" + doc2.name + ".chairs", - "xfilter-" + doc2.name + ".all", - "xfilter-" + doc3.name, - "xfilter-" + doc3.name + ".ad", - "xfilter-" + doc3.name + ".authors", - "xfilter-" + doc3.name + ".chairs", - "xfilter-" + doc5.name, - "xfilter-" + doc5.name + ".authors", - "xfilter-" + doc5.name + ".all", - ]: - self.assertIn(x, acontent) - - for x in [ - "xfilter-" + doc1.name + ".chairs", - "xfilter-" + doc2.name + ".shepherd", - "xfilter-" + doc3.name + ".shepherd", - "xfilter-" + doc4.name, - "xfilter-" + doc5.name + ".shepherd", - "xfilter-" + doc5.name + ".ad", - ]: - self.assertNotIn(x, acontent) - - with open(settings.DRAFT_VIRTUAL_PATH) as vfile: - vcontent = vfile.read() - for x in [ - ad.email_address(), - shepherd.email_address(), - marschairman.email_address(), - author1.email_address(), - author2.email_address(), - author3.email_address(), - author6.email_address(), - ]: - self.assertIn(x, vcontent) - - for x in [ - author4.email_address(), - author5.email_address(), - ]: - self.assertNotIn(x, vcontent) - - for x in [ - "xfilter-" + doc1.name, - "xfilter-" + doc1.name + ".ad", - "xfilter-" + doc1.name + ".authors", - "xfilter-" + doc1.name + ".shepherd", - "xfilter-" + doc1.name + ".all", - "xfilter-" + doc2.name, - "xfilter-" + doc2.name + ".ad", - "xfilter-" + doc2.name + ".authors", - "xfilter-" + doc2.name + ".chairs", - "xfilter-" + doc2.name + ".all", - "xfilter-" + doc3.name, - "xfilter-" + doc3.name + ".ad", - "xfilter-" + doc3.name + ".authors", - "xfilter-" + doc3.name + ".chairs", - "xfilter-" + doc3.name + ".all", - "xfilter-" + doc5.name, - "xfilter-" + doc5.name + ".authors", - "xfilter-" + doc5.name + ".all", - ]: - self.assertIn(x, vcontent) - - for x in [ - "xfilter-" + doc1.name + ".chairs", - "xfilter-" + doc2.name + ".shepherd", - "xfilter-" + doc3.name + ".shepherd", - "xfilter-" + doc4.name, - "xfilter-" + doc5.name + ".shepherd", - "xfilter-" + doc5.name + ".ad", - ]: - self.assertNotIn(x, vcontent) - @override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org") def test_generator_class(self): """The DraftAliasGenerator should generate the same lists as the old mgmt cmd""" @@ -2426,6 +2268,28 @@ def test_generator_class(self): {k: sorted(v) for k, v in expected_dict.items()}, ) + # check single name + output = [(alias, alist) for alias, alist in DraftAliasGenerator(Document.objects.filter(name=doc1.name))] + alias_dict = dict(output) + self.assertEqual(len(alias_dict), len(output)) # no duplicate aliases + expected_dict = { + doc1.name: [author1.email_address()], + doc1.name + ".ad": [ad.email_address()], + doc1.name + ".authors": [author1.email_address()], + doc1.name + ".shepherd": [shepherd.email_address()], + doc1.name + + ".all": [ + author1.email_address(), + ad.email_address(), + shepherd.email_address(), + ], + } + # Sort lists for comparison + self.assertEqual( + {k: sorted(v) for k, v in alias_dict.items()}, + {k: sorted(v) for k, v in expected_dict.items()}, + ) + @override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org") def test_get_draft_notify_emails(self): ad = PersonFactory() @@ -2476,37 +2340,20 @@ def setUp(self): WgDraftFactory(name='draft-ietf-mars-test',group__acronym='mars') WgDraftFactory(name='draft-ietf-ames-test',group__acronym='ames') RoleFactory(group__type_id='review', group__acronym='yangdoctors', name_id='secr') - self.doc_alias_file = NamedTemporaryFile(delete=False, mode='w+') - self.doc_alias_file.write("""# Generated by hand at 2015-02-12_16:26:45 -virtual.ietf.org anything -draft-ietf-mars-test@ietf.org xfilter-draft-ietf-mars-test -expand-draft-ietf-mars-test@virtual.ietf.org mars-author@example.com, mars-collaborator@example.com -draft-ietf-mars-test.authors@ietf.org xfilter-draft-ietf-mars-test.authors -expand-draft-ietf-mars-test.authors@virtual.ietf.org mars-author@example.mars, mars-collaborator@example.mars -draft-ietf-mars-test.chairs@ietf.org xfilter-draft-ietf-mars-test.chairs -expand-draft-ietf-mars-test.chairs@virtual.ietf.org mars-chair@example.mars -draft-ietf-mars-test.all@ietf.org xfilter-draft-ietf-mars-test.all -expand-draft-ietf-mars-test.all@virtual.ietf.org mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars -draft-ietf-ames-test@ietf.org xfilter-draft-ietf-ames-test -expand-draft-ietf-ames-test@virtual.ietf.org ames-author@example.com, ames-collaborator@example.com -draft-ietf-ames-test.authors@ietf.org xfilter-draft-ietf-ames-test.authors -expand-draft-ietf-ames-test.authors@virtual.ietf.org ames-author@example.ames, ames-collaborator@example.ames -draft-ietf-ames-test.chairs@ietf.org xfilter-draft-ietf-ames-test.chairs -expand-draft-ietf-ames-test.chairs@virtual.ietf.org ames-chair@example.ames -draft-ietf-ames-test.all@ietf.org xfilter-draft-ietf-ames-test.all -expand-draft-ietf-ames-test.all@virtual.ietf.org ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames - -""") - self.doc_alias_file.close() - self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH - settings.DRAFT_VIRTUAL_PATH = self.doc_alias_file.name - - def tearDown(self): - settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path - os.unlink(self.doc_alias_file.name) - super().tearDown() - - def testAliases(self): + + + @mock.patch("ietf.doc.views_doc.get_doc_email_aliases") + def testAliases(self, mock_get_aliases): + mock_get_aliases.return_value = [ + {"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"}, + {"doc_name": "draft-ietf-ames-test", "alias_type": "", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"}, + {"doc_name": "draft-ietf-ames-test", "alias_type": ".authors", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"}, + {"doc_name": "draft-ietf-ames-test", "alias_type": ".chairs", "expansion": "ames-chair@example.ames"}, + {"doc_name": "draft-ietf-ames-test", "alias_type": ".all", "expansion": "ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames"}, + ] PersonFactory(user__username='plain') url = urlreverse('ietf.doc.urls.redirect.document_email', kwargs=dict(name="draft-ietf-mars-test")) r = self.client.get(url) @@ -2516,16 +2363,70 @@ def testAliases(self): login_testing_unauthorized(self, "plain", url) r = self.client.get(url) self.assertEqual(r.status_code, 200) + self.assertEqual(mock_get_aliases.call_args, mock.call()) self.assertTrue(all([x in unicontent(r) for x in ['mars-test@','mars-test.authors@','mars-test.chairs@']])) self.assertTrue(all([x in unicontent(r) for x in ['ames-test@','ames-test.authors@','ames-test.chairs@']])) - def testExpansions(self): + + @mock.patch("ietf.doc.views_doc.get_doc_email_aliases") + def testExpansions(self, mock_get_aliases): + mock_get_aliases.return_value = [ + {"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"}, + {"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"}, + ] url = urlreverse('ietf.doc.views_doc.document_email', kwargs=dict(name="draft-ietf-mars-test")) r = self.client.get(url) + self.assertEqual(mock_get_aliases.call_args, mock.call("draft-ietf-mars-test")) self.assertEqual(r.status_code, 200) self.assertContains(r, 'draft-ietf-mars-test.all@ietf.org') self.assertContains(r, 'iesg_ballot_saved') + + @mock.patch("ietf.doc.utils.DraftAliasGenerator") + def test_get_doc_email_aliases(self, mock_alias_gen_cls): + mock_alias_gen_cls.return_value = [ + ("draft-something-or-other.some-type", ["somebody@example.com"]), + ("draft-something-or-other", ["somebody@example.com"]), + ("draft-nothing-at-all", ["nobody@example.com"]), + ("draft-nothing-at-all.some-type", ["nobody@example.com"]), + ] + # order is important in the response - should be sorted by doc name and otherwise left + # in order + self.assertEqual( + get_doc_email_aliases(), + [ + { + "doc_name": "draft-nothing-at-all", + "alias_type": "", + "expansion": "nobody@example.com", + }, + { + "doc_name": "draft-nothing-at-all", + "alias_type": ".some-type", + "expansion": "nobody@example.com", + }, + { + "doc_name": "draft-something-or-other", + "alias_type": ".some-type", + "expansion": "somebody@example.com", + }, + { + "doc_name": "draft-something-or-other", + "alias_type": "", + "expansion": "somebody@example.com", + }, + ], + ) + self.assertEqual(mock_alias_gen_cls.call_args, mock.call(None)) + # Repeat with a name, no need to re-test that the alias list is actually passed through, just + # check that the DraftAliasGenerator is called correctly + draft = WgDraftFactory() + get_doc_email_aliases(draft.name) + self.assertQuerySetEqual(mock_alias_gen_cls.call_args[0][0], Document.objects.filter(pk=draft.pk)) + + class DocumentMeetingTests(TestCase): def setUp(self): @@ -2977,32 +2878,40 @@ def test_markdown_and_text(self): class Idnits2SupportTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['DERIVED_DIR'] - def test_obsoleted(self): + def test_generate_idnits2_rfcs_obsoleted(self): rfc = WgRfcFactory(rfc_number=1001) WgRfcFactory(rfc_number=1003,relations=[('obs',rfc)]) rfc = WgRfcFactory(rfc_number=1005) WgRfcFactory(rfc_number=1007,relations=[('obs',rfc)]) + blob = generate_idnits2_rfcs_obsoleted() + self.assertEqual(blob, b'1001 1003\n1005 1007\n'.decode("utf8")) + def test_obsoleted(self): url = urlreverse('ietf.doc.views_doc.idnits2_rfcs_obsoleted') r = self.client.get(url) self.assertEqual(r.status_code, 404) - call_command('generate_idnits2_rfcs_obsoleted') + # value written is arbitrary, expect it to be passed through + (Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted").write_bytes(b'1001 1003\n1005 1007\n') url = urlreverse('ietf.doc.views_doc.idnits2_rfcs_obsoleted') r = self.client.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(r.content, b'1001 1003\n1005 1007\n') - def test_rfc_status(self): + def test_generate_idnits2_rfc_status(self): for slug in ('bcp', 'ds', 'exp', 'hist', 'inf', 'std', 'ps', 'unkn'): WgRfcFactory(std_level_id=slug) + blob = generate_idnits2_rfc_status().replace("\n", "") + self.assertEqual(blob[6312-1], "O") + + def test_rfc_status(self): url = urlreverse('ietf.doc.views_doc.idnits2_rfc_status') r = self.client.get(url) self.assertEqual(r.status_code,404) - call_command('generate_idnits2_rfc_status') + # value written is arbitrary, expect it to be passed through + (Path(settings.DERIVED_DIR) / "idnits2-rfc-status").write_bytes(b'1001 1003\n1005 1007\n') r = self.client.get(url) self.assertEqual(r.status_code,200) - blob = unicontent(r).replace('\n','') - self.assertEqual(blob[6312-1],'O') + self.assertEqual(r.content, b'1001 1003\n1005 1007\n') def test_idnits2_state(self): rfc = WgRfcFactory() @@ -3273,3 +3182,151 @@ def test_referenced_by_rfcs_as_rfc_or_draft(self): rfc.referenced_by_rfcs_as_rfc_or_draft(), draft.targets_related.filter(source__type="rfc") | rfc.targets_related.filter(source__type="rfc"), ) + +class StateIndexTests(TestCase): + + def test_state_index(self): + url = urlreverse('ietf.doc.views_help.state_index') + r = self.client.get(url) + q = PyQuery(r.content) + content = [ e.text for e in q('#content table td a ') ] + names = StateType.objects.values_list('slug', flat=True) + # The following doesn't cover all doc types, only a selection + for name in names: + if not '-' in name: + self.assertIn(name, content) + +class InvestigateTests(TestCase): + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ + "AGENDA_PATH", + # "INTERNET_DRAFT_PATH", + # "INTERNET_DRAFT_ARCHIVE_DIR", + # "INTERNET_ALL_DRAFTS_ARCHIVE_DIR", + ] + + def setUp(self): + super().setUp() + # Contort the draft archive dir temporary replacement + # to match the "collections" concept + archive_tmp_dir = Path(settings.INTERNET_DRAFT_ARCHIVE_DIR) + new_archive_dir = archive_tmp_dir / "draft-archive" + new_archive_dir.mkdir() + settings.INTERNET_DRAFT_ARCHIVE_DIR = str(new_archive_dir) + donated_personal_copy_dir = archive_tmp_dir / "donated-personal-copy" + donated_personal_copy_dir.mkdir() + meeting_dir = Path(settings.AGENDA_PATH) / "666" + meeting_dir.mkdir() + all_archive_dir = Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR) + repository_dir = Path(settings.INTERNET_DRAFT_PATH) + + for path in [repository_dir, all_archive_dir]: + (path / "draft-this-is-active-00.txt").touch() + for path in [new_archive_dir, all_archive_dir]: + (path / "draft-old-but-can-authenticate-00.txt").touch() + (path / "draft-has-mixed-provenance-01.txt").touch() + for path in [donated_personal_copy_dir, all_archive_dir]: + (path / "draft-donated-from-a-personal-collection-00.txt").touch() + (path / "draft-has-mixed-provenance-00.txt").touch() + (path / "draft-has-mixed-provenance-00.txt.Z").touch() + (all_archive_dir / "draft-this-should-not-be-possible-00.txt").touch() + (meeting_dir / "draft-this-predates-the-archive-00.txt").touch() + + def test_investigate_fragment(self): + + result = investigate_fragment("this-is-active") + self.assertEqual(len(result["can_verify"]), 1) + self.assertEqual(len(result["unverifiable_collections"]), 0) + self.assertEqual(len(result["unexpected"]), 0) + self.assertEqual( + list(result["can_verify"])[0].name, "draft-this-is-active-00.txt" + ) + + result = investigate_fragment("old-but-can") + self.assertEqual(len(result["can_verify"]), 1) + self.assertEqual(len(result["unverifiable_collections"]), 0) + self.assertEqual(len(result["unexpected"]), 0) + self.assertEqual( + list(result["can_verify"])[0].name, "draft-old-but-can-authenticate-00.txt" + ) + + result = investigate_fragment("predates") + self.assertEqual(len(result["can_verify"]), 1) + self.assertEqual(len(result["unverifiable_collections"]), 0) + self.assertEqual(len(result["unexpected"]), 0) + self.assertEqual( + list(result["can_verify"])[0].name, "draft-this-predates-the-archive-00.txt" + ) + + result = investigate_fragment("personal-collection") + self.assertEqual(len(result["can_verify"]), 0) + self.assertEqual(len(result["unverifiable_collections"]), 1) + self.assertEqual(len(result["unexpected"]), 0) + self.assertEqual( + list(result["unverifiable_collections"])[0].name, + "draft-donated-from-a-personal-collection-00.txt", + ) + + result = investigate_fragment("mixed-provenance") + self.assertEqual(len(result["can_verify"]), 1) + self.assertEqual(len(result["unverifiable_collections"]), 2) + self.assertEqual(len(result["unexpected"]), 0) + self.assertEqual( + list(result["can_verify"])[0].name, "draft-has-mixed-provenance-01.txt" + ) + self.assertEqual( + set([p.name for p in result["unverifiable_collections"]]), + set( + [ + "draft-has-mixed-provenance-00.txt", + "draft-has-mixed-provenance-00.txt.Z", + ] + ), + ) + + result = investigate_fragment("not-be-possible") + self.assertEqual(len(result["can_verify"]), 0) + self.assertEqual(len(result["unverifiable_collections"]), 0) + self.assertEqual(len(result["unexpected"]), 1) + self.assertEqual( + list(result["unexpected"])[0].name, + "draft-this-should-not-be-possible-00.txt", + ) + + def test_investigate(self): + url = urlreverse("ietf.doc.views_doc.investigate") + login_testing_unauthorized(self, "secretary", url) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("form#investigate")), 1) + self.assertEqual(len(q("div#results")), 0) + r = self.client.post(url, dict(name_fragment="this-is-not-found")) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("div#results")), 1) + self.assertEqual(len(q("table#authenticated")), 0) + self.assertEqual(len(q("table#unverifiable")), 0) + self.assertEqual(len(q("table#unexpected")), 0) + r = self.client.post(url, dict(name_fragment="mixed-provenance")) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("div#results")), 1) + self.assertEqual(len(q("table#authenticated")), 1) + self.assertEqual(len(q("table#unverifiable")), 1) + self.assertEqual(len(q("table#unexpected")), 0) + r = self.client.post(url, dict(name_fragment="not-be-possible")) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("div#results")), 1) + self.assertEqual(len(q("table#authenticated")), 0) + self.assertEqual(len(q("table#unverifiable")), 0) + self.assertEqual(len(q("table#unexpected")), 1) + r = self.client.post(url, dict(name_fragment="short")) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("#id_name_fragment.is-invalid")), 1) + for char in ["*", "%", "/", "\\"]: + r = self.client.post(url, dict(name_fragment=f"bad{char}character")) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q("#id_name_fragment.is-invalid")), 1) diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py index 9c9287dab2..e18b2abfd9 100644 --- a/ietf/doc/tests_ballot.py +++ b/ietf/doc/tests_ballot.py @@ -32,7 +32,7 @@ from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.text import unwrap -from ietf.utils.timezone import date_today +from ietf.utils.timezone import date_today, datetime_today class EditPositionTests(TestCase): @@ -529,6 +529,7 @@ def test_issue_ballot_warn_if_early(self): login_testing_unauthorized(self, "secretary", url) # expect warning about issuing a ballot before IETF Last Call is done + # No last call has yet been issued r = self.client.get(url) self.assertEqual(r.status_code, 200) q = PyQuery(r.content) @@ -536,6 +537,38 @@ def test_issue_ballot_warn_if_early(self): self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) self.assertTrue(q('[type=submit]:contains("Save")')) + # Last call exists but hasn't expired + LastCallDocEvent.objects.create( + doc=draft, + expires=datetime_today()+datetime.timedelta(days=14), + by=Person.objects.get(name="(System)") + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('[class=text-danger]:contains("not completed IETF Last Call")')) + + # Last call exists and has expired + LastCallDocEvent.objects.filter(doc=draft).update(expires=datetime_today()-datetime.timedelta(days=2)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertFalse(q('[class=text-danger]:contains("not completed IETF Last Call")')) + + for state_slug in ["lc", "watching", "ad-eval"]: + draft.set_state(State.objects.get(type="draft-iesg",slug=state_slug)) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) + + draft.set_state(State.objects.get(type="draft-iesg",slug="writeupw")) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertFalse(q('[class=text-danger]:contains("It would be unexpected to issue a ballot while in this state.")')) + + def test_edit_approval_text(self): ad = Person.objects.get(user__username="ad") draft = WgDraftFactory(ad=ad,states=[('draft','active'),('draft-iesg','iesg-eva')],intended_std_level_id='ps',group__parent=Group.objects.get(acronym='farfut')) diff --git a/ietf/doc/tests_charter.py b/ietf/doc/tests_charter.py index 1bd6c1701d..e0207fe842 100644 --- a/ietf/doc/tests_charter.py +++ b/ietf/doc/tests_charter.py @@ -87,6 +87,10 @@ def test_view_revisions(self): class EditCharterTests(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['CHARTER_PATH'] + def setUp(self): + super().setUp() + (Path(settings.FTP_DIR)/"charter").mkdir() + def write_charter_file(self, charter): (Path(settings.CHARTER_PATH) / f"{charter.name}-{charter.rev}.txt").write_text("This is a charter.") @@ -506,13 +510,16 @@ def test_submit_charter(self): self.assertEqual(charter.rev, next_revision(prev_rev)) self.assertTrue("new_revision" in charter.latest_event().type) - file_contents = ( - Path(settings.CHARTER_PATH) / (charter.name + "-" + charter.rev + ".txt") - ).read_text("utf-8") + charter_path = Path(settings.CHARTER_PATH) / (charter.name + "-" + charter.rev + ".txt") + file_contents = (charter_path).read_text("utf-8") self.assertEqual( file_contents, "Windows line\nMac line\nUnix line\n" + utf_8_snippet.decode("utf-8"), ) + ftp_charter_path = Path(settings.FTP_DIR) / "charter" / charter_path.name + self.assertTrue(ftp_charter_path.exists()) + self.assertTrue(charter_path.samefile(ftp_charter_path)) + def test_submit_initial_charter(self): group = GroupFactory(type_id='wg',acronym='mars',list_email='mars-wg@ietf.org') @@ -808,9 +815,11 @@ def test_approve(self): self.assertTrue(not charter.ballot_open("approve")) self.assertEqual(charter.rev, "01") - self.assertTrue( - (Path(settings.CHARTER_PATH) / ("charter-ietf-%s-%s.txt" % (group.acronym, charter.rev))).exists() - ) + charter_path = Path(settings.CHARTER_PATH) / ("charter-ietf-%s-%s.txt" % (group.acronym, charter.rev)) + charter_ftp_path = Path(settings.FTP_DIR) / "charter" / charter_path.name + self.assertTrue(charter_path.exists()) + self.assertTrue(charter_ftp_path.exists()) + self.assertTrue(charter_path.samefile(charter_ftp_path)) self.assertEqual(len(outbox), 2) # diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index d9aca94e86..a956fd3287 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- +from pathlib import Path import datetime, os, shutil import io import tarfile, tempfile, mailbox @@ -47,6 +48,7 @@ def setUp(self): self.review_dir = self.tempdir('review') self.old_document_path_pattern = settings.DOCUMENT_PATH_PATTERN settings.DOCUMENT_PATH_PATTERN = self.review_dir + "/{doc.type_id}/" + (Path(settings.FTP_DIR) / "review").mkdir() self.review_subdir = os.path.join(self.review_dir, "review") if not os.path.exists(self.review_subdir): @@ -57,6 +59,13 @@ def tearDown(self): settings.DOCUMENT_PATH_PATTERN = self.old_document_path_pattern super().tearDown() + def verify_review_files_were_written(self, assignment, expected_content = "This is a review\nwith two lines"): + review_file = Path(self.review_subdir) / f"{assignment.review.name}.txt" + content = review_file.read_text() + self.assertEqual(content, expected_content) + review_ftp_file = Path(settings.FTP_DIR) / "review" / review_file.name + self.assertTrue(review_file.samefile(review_ftp_file)) + def test_request_review(self): doc = WgDraftFactory(group__acronym='mars',rev='01') NewRevisionDocEventFactory(doc=doc,rev='01') @@ -830,8 +839,7 @@ def test_complete_review_upload_content(self): self.assertTrue(assignment.review_request.team.acronym.lower() in assignment.review.name) self.assertTrue(assignment.review_request.doc.rev in assignment.review.name) - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 1) self.assertIn(assignment.review_request.team.list_email, outbox[0]["To"]) @@ -885,8 +893,7 @@ def test_complete_review_enter_content(self): completed_time_diff = timezone.now() - assignment.completed_on self.assertLess(completed_time_diff, datetime.timedelta(seconds=10)) - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 1) self.assertIn(assignment.review_request.team.list_email, outbox[0]["To"]) @@ -926,8 +933,7 @@ def test_complete_review_enter_content_by_secretary(self): self.assertLess(event0_time_diff, datetime.timedelta(seconds=10)) self.assertEqual(events[1].time, datetime.datetime(2012, 12, 24, 12, 13, 14, tzinfo=DEADLINE_TZINFO)) - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 1) self.assertIn(assignment.review_request.team.list_email, outbox[0]["To"]) @@ -1013,8 +1019,7 @@ def test_complete_review_link_to_mailing_list(self, mock): assignment = reload_db_objects(assignment) self.assertEqual(assignment.state_id, "completed") - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 0) self.assertTrue("http://example.com" in assignment.review.external_url) @@ -1063,8 +1068,7 @@ def test_complete_unsolicited_review_link_to_mailing_list_by_secretary(self, moc self.assertEqual(assignment.reviewer, rev_role.person.role_email('reviewer')) self.assertEqual(assignment.state_id, "completed") - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 0) self.assertTrue("http://example.com" in assignment.review.external_url) @@ -1172,8 +1176,9 @@ def test_revise_review_enter_content(self): self.assertLess(event_time_diff, datetime.timedelta(seconds=10)) self.assertTrue('revised' in event1.desc.lower()) - with io.open(os.path.join(self.review_subdir, assignment.review.name + ".txt")) as f: - self.assertEqual(f.read(), "This is a review\nwith two lines") + # See https://github.com/ietf-tools/datatracker/issues/6941 + # These are _not_ getting written as a new version as intended. + self.verify_review_files_were_written(assignment) self.assertEqual(len(outbox), 0) @@ -1200,6 +1205,8 @@ def test_revise_review_enter_content(self): # Ensure that a new event was created for the new revision (#2590) self.assertNotEqual(event1.id, event2.id) + self.verify_review_files_were_written(assignment, "This is a revised review") + self.assertEqual(len(outbox), 0) def test_edit_comment(self): diff --git a/ietf/doc/tests_tasks.py b/ietf/doc/tests_tasks.py index 931ed438dc..51a8556e69 100644 --- a/ietf/doc/tests_tasks.py +++ b/ietf/doc/tests_tasks.py @@ -1,15 +1,28 @@ # Copyright The IETF Trust 2024, All Rights Reserved +import datetime import mock +from pathlib import Path + +from django.conf import settings +from django.utils import timezone + from ietf.utils.test_utils import TestCase from ietf.utils.timezone import datetime_today -from .factories import DocumentFactory -from .models import Document -from .tasks import expire_ids_task, notify_expirations_task - +from .factories import DocumentFactory, NewRevisionDocEventFactory +from .models import Document, NewRevisionDocEvent +from .tasks import ( + expire_ids_task, + expire_last_calls_task, + generate_draft_bibxml_files_task, + generate_idnits2_rfcs_obsoleted_task, + generate_idnits2_rfc_status_task, + notify_expirations_task, +) class TaskTests(TestCase): + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ["DERIVED_DIR"] @mock.patch("ietf.doc.tasks.in_draft_expire_freeze") @mock.patch("ietf.doc.tasks.get_expired_drafts") @@ -35,10 +48,10 @@ def test_expire_ids_task( Document.objects.filter(pk=doc.pk), Document.objects.filter(pk=other_doc.pk), ] - + # call task expire_ids_task() - + # check results self.assertTrue(in_draft_expire_freeze_mock.called) self.assertEqual(expirable_drafts_mock.call_count, 2) @@ -50,7 +63,7 @@ def test_expire_ids_task( # test that an exception is raised in_draft_expire_freeze_mock.side_effect = RuntimeError - with self.assertRaises(RuntimeError):( + with self.assertRaises(RuntimeError): ( expire_ids_task()) @mock.patch("ietf.doc.tasks.send_expire_warning_for_draft") @@ -61,3 +74,129 @@ def test_notify_expirations_task(self, get_drafts_mock, send_warning_mock): notify_expirations_task() self.assertEqual(send_warning_mock.call_count, 1) self.assertEqual(send_warning_mock.call_args[0], ("sentinel",)) + + @mock.patch("ietf.doc.tasks.expire_last_call") + @mock.patch("ietf.doc.tasks.get_expired_last_calls") + def test_expire_last_calls_task(self, mock_get_expired, mock_expire): + docs = DocumentFactory.create_batch(3) + mock_get_expired.return_value = docs + expire_last_calls_task() + self.assertTrue(mock_get_expired.called) + self.assertEqual(mock_expire.call_count, 3) + self.assertEqual(mock_expire.call_args_list[0], mock.call(docs[0])) + self.assertEqual(mock_expire.call_args_list[1], mock.call(docs[1])) + self.assertEqual(mock_expire.call_args_list[2], mock.call(docs[2])) + + # Check that it runs even if exceptions occur + mock_get_expired.reset_mock() + mock_expire.reset_mock() + mock_expire.side_effect = ValueError + expire_last_calls_task() + self.assertTrue(mock_get_expired.called) + self.assertEqual(mock_expire.call_count, 3) + self.assertEqual(mock_expire.call_args_list[0], mock.call(docs[0])) + self.assertEqual(mock_expire.call_args_list[1], mock.call(docs[1])) + self.assertEqual(mock_expire.call_args_list[2], mock.call(docs[2])) + + @mock.patch("ietf.doc.tasks.generate_idnits2_rfc_status") + def test_generate_idnits2_rfc_status_task(self, mock_generate): + mock_generate.return_value = "dåtå" + generate_idnits2_rfc_status_task() + self.assertEqual(mock_generate.call_count, 1) + self.assertEqual( + "dåtå".encode("utf8"), + (Path(settings.DERIVED_DIR) / "idnits2-rfc-status").read_bytes(), + ) + + @mock.patch("ietf.doc.tasks.generate_idnits2_rfcs_obsoleted") + def test_generate_idnits2_rfcs_obsoleted_task(self, mock_generate): + mock_generate.return_value = "dåtå" + generate_idnits2_rfcs_obsoleted_task() + self.assertEqual(mock_generate.call_count, 1) + self.assertEqual( + "dåtå".encode("utf8"), + (Path(settings.DERIVED_DIR) / "idnits2-rfcs-obsoleted").read_bytes(), + ) + + @mock.patch("ietf.doc.tasks.ensure_draft_bibxml_path_exists") + @mock.patch("ietf.doc.tasks.update_or_create_draft_bibxml_file") + def test_generate_draft_bibxml_files_task(self, mock_create, mock_ensure_path): + now = timezone.now() + very_old_event = NewRevisionDocEventFactory( + time=now - datetime.timedelta(days=1000), rev="17" + ) + old_event = NewRevisionDocEventFactory( + time=now - datetime.timedelta(days=8), rev="03" + ) + young_event = NewRevisionDocEventFactory( + time=now - datetime.timedelta(days=6), rev="06" + ) + # a couple that should always be ignored + NewRevisionDocEventFactory( + time=now - datetime.timedelta(days=6), rev="09", doc__type_id="rfc" # not a draft + ) + NewRevisionDocEventFactory( + type="changed_document", # not a "new_revision" type + time=now - datetime.timedelta(days=6), + rev="09", + doc__type_id="rfc", + ) + + # Get rid of the "00" events created by the factories -- they're just noise for this test + NewRevisionDocEvent.objects.filter(rev="00").delete() + + # default args - look back 7 days + generate_draft_bibxml_files_task() + self.assertTrue(mock_ensure_path.called) + self.assertCountEqual( + mock_create.call_args_list, [mock.call(young_event.doc, young_event.rev)] + ) + mock_create.reset_mock() + mock_ensure_path.reset_mock() + + # shorter lookback + generate_draft_bibxml_files_task(days=5) + self.assertTrue(mock_ensure_path.called) + self.assertCountEqual(mock_create.call_args_list, []) + mock_create.reset_mock() + mock_ensure_path.reset_mock() + + # longer lookback + generate_draft_bibxml_files_task(days=9) + self.assertTrue(mock_ensure_path.called) + self.assertCountEqual( + mock_create.call_args_list, + [ + mock.call(young_event.doc, young_event.rev), + mock.call(old_event.doc, old_event.rev), + ], + ) + mock_create.reset_mock() + mock_ensure_path.reset_mock() + + # everything + generate_draft_bibxml_files_task(process_all=True) + self.assertTrue(mock_ensure_path.called) + self.assertCountEqual( + mock_create.call_args_list, + [ + mock.call(young_event.doc, young_event.rev), + mock.call(old_event.doc, old_event.rev), + mock.call(very_old_event.doc, very_old_event.rev), + ], + ) + mock_create.reset_mock() + mock_ensure_path.reset_mock() + + # everything should still be tried, even if there's an exception + mock_create.side_effect = RuntimeError + generate_draft_bibxml_files_task(process_all=True) + self.assertTrue(mock_ensure_path.called) + self.assertCountEqual( + mock_create.call_args_list, + [ + mock.call(young_event.doc, young_event.rev), + mock.call(old_event.doc, old_event.rev), + mock.call(very_old_event.doc, very_old_event.rev), + ], + ) diff --git a/ietf/doc/tests_utils.py b/ietf/doc/tests_utils.py index 248ac345af..f610fe3d76 100644 --- a/ietf/doc/tests_utils.py +++ b/ietf/doc/tests_utils.py @@ -2,8 +2,10 @@ import datetime import debug # pyflakes:ignore -from unittest.mock import patch +from pathlib import Path +from unittest.mock import call, patch +from django.conf import settings from django.db import IntegrityError from django.test.utils import override_settings from django.utils import timezone @@ -16,7 +18,8 @@ from ietf.doc.factories import DocumentFactory, WgRfcFactory, WgDraftFactory from ietf.doc.models import State, DocumentActionHolder, DocumentAuthor from ietf.doc.utils import (update_action_holders, add_state_change_event, update_documentauthors, - fuzzy_find_documents, rebuild_reference_relations, build_file_urls) + fuzzy_find_documents, rebuild_reference_relations, build_file_urls, + ensure_draft_bibxml_path_exists, update_or_create_draft_bibxml_file) from ietf.utils.draft import Draft, PlaintextDraft from ietf.utils.xmldraft import XMLDraft @@ -484,3 +487,49 @@ def test_xml_and_plaintext(self, mock_init, mock_get_refs, mock_plaintext_init): (self.updated.name, 'updates'), ] ) + + +class DraftBibxmlTests(TestCase): + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ["BIBXML_BASE_PATH"] + + def test_ensure_draft_bibxml_path_exists(self): + expected = Path(settings.BIBXML_BASE_PATH) / "bibxml-ids" + self.assertFalse(expected.exists()) + ensure_draft_bibxml_path_exists() + self.assertTrue(expected.is_dir()) # false if does not exist or is not dir + + @patch("ietf.doc.utils.bibxml_for_draft", return_value="This\ris\nmy\r\nbibxml") + def test_create_draft_bibxml_file(self, mock): + bibxml_path = Path(settings.BIBXML_BASE_PATH) / "bibxml-ids" + bibxml_path.mkdir(exist_ok=False) # expect to start with a clean slate + + doc = DocumentFactory() + ref_path = bibxml_path / f"reference.I-D.{doc.name}-26.xml" # we're pretending it's rev 26 + + update_or_create_draft_bibxml_file(doc, "26") + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.call_args, call(doc, "26")) + self.assertEqual(ref_path.read_text(), "This\nis\nmy\nbibxml") + + @patch("ietf.doc.utils.bibxml_for_draft", return_value="This\ris\nmy\r\nbibxml") + def test_update_draft_bibxml_file(self, mock): + bibxml_path = Path(settings.BIBXML_BASE_PATH) / "bibxml-ids" + bibxml_path.mkdir(exist_ok=False) # expect to start with a clean slate + + doc = DocumentFactory() + ref_path = bibxml_path / f"reference.I-D.{doc.name}-26.xml" # we're pretending it's rev 26 + ref_path.write_text("Old data") + + # should replace it + update_or_create_draft_bibxml_file(doc, "26") + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.call_args, call(doc, "26")) + self.assertEqual(ref_path.read_text(), "This\nis\nmy\nbibxml") + + # should leave it alone if it differs only by leading/trailing whitespace + mock.reset_mock() + mock.return_value = " \n This\nis\nmy\nbibxml " + update_or_create_draft_bibxml_file(doc, "26") + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.call_args, call(doc, "26")) + self.assertEqual(ref_path.read_text(), "This\nis\nmy\nbibxml") diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index 496b2d8f3a..7d930f7f40 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -66,6 +66,8 @@ r"^shepherdwriteup-template/(?P\w+)/?$", views_doc.document_shepherd_writeup_template, ), + url(r'^investigate/?$', views_doc.investigate), + url(r'^stats/newrevisiondocevent/?$', views_stats.chart_newrevisiondocevent), url(r'^stats/newrevisiondocevent/conf/?$', views_stats.chart_conf_newrevisiondocevent), @@ -163,6 +165,7 @@ url(r'^%(name)s/edit/issueballot/rsab/$' % settings.URL_REGEXPS, views_ballot.issue_rsab_ballot), url(r'^%(name)s/edit/closeballot/rsab/$' % settings.URL_REGEXPS, views_ballot.close_rsab_ballot), + url(r'^help/state/?$', views_help.state_index), url(r'^help/state/(?P[\w-]+)/$', views_help.state_help), url(r'^help/relationships/$', views_help.relationship_help), url(r'^help/relationships/(?P\w+)/$', views_help.relationship_help), @@ -178,7 +181,8 @@ url(r'^%(name)s/session/' % settings.URL_REGEXPS, include('ietf.doc.urls_material')), url(r'^(?P[A-Za-z0-9._+-]+)/session/', include(session_patterns)), url(r'^(?P[A-Za-z0-9\._\+\-]+)$', views_search.search_for_name), - # latest versions - keep old URLs alive during migration period + # rfcdiff - latest versions - keep old URLs alive during migration period url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, RedirectView.as_view(pattern_name='ietf.api.views.rfcdiff_latest_json', permanent=True)), url(r'^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', RedirectView.as_view(pattern_name='ietf.api.views.rfcdiff_latest_json', permanent=True)), + # end of rfcdiff support URLs ] diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index ad1c2af223..cd0fbb43b0 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -13,11 +13,13 @@ from collections import defaultdict, namedtuple, Counter from dataclasses import dataclass -from typing import Iterator, Union +from pathlib import Path +from typing import Iterator, Optional, Union from zoneinfo import ZoneInfo from django.conf import settings from django.contrib import messages +from django.db.models import OuterRef from django.forms import ValidationError from django.http import Http404 from django.template.loader import render_to_string @@ -38,7 +40,7 @@ from ietf.name.models import DocReminderTypeName, DocRelationshipName from ietf.group.models import Role, Group, GroupFeatures from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_individual_draft_author, is_bofreq_editor -from ietf.person.models import Person +from ietf.person.models import Email, Person from ietf.review.models import ReviewWish from ietf.utils import draft, log from ietf.utils.mail import parseaddr, send_mail @@ -1062,6 +1064,8 @@ def build_file_urls(doc: Union[Document, DocHistory]): base = settings.IETF_ID_ARCHIVE_URL file_urls = [] for t in found_types: + if t == "ps": # Postscript might have been submitted but should not be displayed in the list of URLs + continue label = "plain text" if t == "txt" else t file_urls.append((label, base + doc.name + "-" + doc.rev + "." + t)) @@ -1262,6 +1266,12 @@ def bibxml_for_draft(doc, rev=None): class DraftAliasGenerator: days = 2 * 365 + def __init__(self, draft_queryset=None): + if draft_queryset is not None: + self.draft_queryset = draft_queryset.filter(type_id="draft") # only drafts allowed + else: + self.draft_queryset = Document.objects.filter(type_id="draft") + def get_draft_ad_emails(self, doc): """Get AD email addresses for the given draft, if any.""" from ietf.group.utils import get_group_ad_emails # avoid circular import @@ -1292,9 +1302,13 @@ def get_draft_shepherd_email(self, doc): def get_draft_authors_emails(self, doc): """Get list of authors for the given draft.""" author_emails = set() - for author in doc.documentauthor_set.all(): - if author.email and author.email.email_address(): - author_emails.add(author.email.email_address()) + for email in Email.objects.filter(documentauthor__document=doc): + if email.active: + author_emails.add(email.address) + elif email.person: + person_email = email.person.email_address() + if person_email: + author_emails.add(person_email) return author_emails def get_draft_notify_emails(self, doc): @@ -1327,56 +1341,144 @@ def get_draft_notify_emails(self, doc): notify_emails.add(email) return notify_emails + def _yield_aliases_for_draft(self, doc)-> Iterator[tuple[str, list[str]]]: + alias = doc.name + all = set() + + # no suffix and .authors are the same list + emails = self.get_draft_authors_emails(doc) + all.update(emails) + if emails: + yield alias, list(emails) + yield alias + ".authors", list(emails) + + # .chairs = group chairs + emails = self.get_draft_chair_emails(doc) + if emails: + all.update(emails) + yield alias + ".chairs", list(emails) + + # .ad = sponsoring AD / WG AD (WG document) + emails = self.get_draft_ad_emails(doc) + if emails: + all.update(emails) + yield alias + ".ad", list(emails) + + # .notify = notify email list from the Document + emails = self.get_draft_notify_emails(doc) + if emails: + all.update(emails) + yield alias + ".notify", list(emails) + + # .shepherd = shepherd email from the Document + emails = self.get_draft_shepherd_email(doc) + if emails: + all.update(emails) + yield alias + ".shepherd", list(emails) + + # .all = everything from above + if all: + yield alias + ".all", list(all) + def __iter__(self) -> Iterator[tuple[str, list[str]]]: # Internet-Drafts with active status or expired within self.days show_since = timezone.now() - datetime.timedelta(days=self.days) - drafts = Document.objects.filter(type_id="draft") - active_drafts = drafts.filter(states__slug='active') - inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since) - interesting_drafts = active_drafts | inactive_recent_drafts - - for this_draft in interesting_drafts.distinct().iterator(): + drafts = self.draft_queryset + + # Look up the draft-active state properly. Doing this with + # states__type_id, states__slug directly in the `filter()` + # works, but it does not work as expected in `exclude()`. + active_state = State.objects.get(type_id="draft", slug="active") + active_pks = [] # build a static list of the drafts we actually returned as "active" + active_drafts = drafts.filter(states=active_state) + for this_draft in active_drafts: + active_pks.append(this_draft.pk) + for alias, addresses in self._yield_aliases_for_draft(this_draft): + yield alias, addresses + + # Annotate with the draft state slug so we can check for drafts that + # have become RFCs + inactive_recent_drafts = ( + drafts.exclude(pk__in=active_pks) # don't re-filter by state, states may have changed during the run! + .filter(expires__gte=show_since) + .annotate( + # Why _default_manager instead of objects? See: + # https://docs.djangoproject.com/en/4.2/topics/db/managers/#django.db.models.Model._default_manager + draft_state_slug=Document.states.through._default_manager.filter( + document__pk=OuterRef("pk"), + state__type_id="draft" + ).values("state__slug"), + ) + ) + for this_draft in inactive_recent_drafts: # Omit drafts that became RFCs, unless they were published in the last DEFAULT_YEARS - if this_draft.get_state_slug() == "rfc": + if this_draft.draft_state_slug == "rfc": rfc = this_draft.became_rfc() log.assertion("rfc is not None") if rfc.latest_event(type='published_rfc').time < show_since: continue + for alias, addresses in self._yield_aliases_for_draft(this_draft): + yield alias, addresses - alias = this_draft.name - all = set() - # no suffix and .authors are the same list - emails = self.get_draft_authors_emails(this_draft) - all.update(emails) - if emails: - yield alias, list(emails) - yield alias + ".authors", list(emails) - - # .chairs = group chairs - emails = self.get_draft_chair_emails(this_draft) - if emails: - all.update(emails) - yield alias + ".chairs", list(emails) - - # .ad = sponsoring AD / WG AD (WG document) - emails = self.get_draft_ad_emails(this_draft) - if emails: - all.update(emails) - yield alias + ".ad", list(emails) - - # .notify = notify email list from the Document - emails = self.get_draft_notify_emails(this_draft) - if emails: - all.update(emails) - yield alias + ".notify", list(emails) - - # .shepherd = shepherd email from the Document - emails = self.get_draft_shepherd_email(this_draft) - if emails: - all.update(emails) - yield alias + ".shepherd", list(emails) - - # .all = everything from above - if all: - yield alias + ".all", list(all) +def get_doc_email_aliases(name: Optional[str] = None): + aliases = [] + for (alias, alist) in DraftAliasGenerator( + Document.objects.filter(type_id="draft", name=name) if name else None + ): + # alias is draft-name.alias_type + doc_name, _dot, alias_type = alias.partition(".") + aliases.append({ + "doc_name": doc_name, + "alias_type": f".{alias_type}" if alias_type else "", + "expansion": ", ".join(sorted(alist)), + }) + return sorted(aliases, key=lambda a: (a["doc_name"])) + + +def investigate_fragment(name_fragment): + can_verify = set() + for root in [settings.INTERNET_DRAFT_PATH, settings.INTERNET_DRAFT_ARCHIVE_DIR]: + can_verify.update(list(Path(root).glob(f"*{name_fragment}*"))) + archive_verifiable_names = set([p.name for p in can_verify]) + # Can also verify drafts in proceedings directories + can_verify.update(list(Path(settings.AGENDA_PATH).glob(f"**/*{name_fragment}*"))) + + # N.B. This reflects the assumption that the internet draft archive dir is in the + # a directory with other collections (at /a/ietfdata/draft/collections as this is written) + unverifiable_collections = set([ + p for p in + Path(settings.INTERNET_DRAFT_ARCHIVE_DIR).parent.glob(f"**/*{name_fragment}*") + if p.name not in archive_verifiable_names + ]) + + unverifiable_collections.difference_update(can_verify) + + expected_names = set([p.name for p in can_verify.union(unverifiable_collections)]) + maybe_unexpected = list( + Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR).glob(f"*{name_fragment}*") + ) + unexpected = [p for p in maybe_unexpected if p.name not in expected_names] + + return dict( + can_verify=can_verify, + unverifiable_collections=unverifiable_collections, + unexpected=unexpected, + ) + + +def update_or_create_draft_bibxml_file(doc, rev): + log.assertion("doc.type_id == 'draft'") + normalized_bibxml = re.sub(r"\r\n?", r"\n", bibxml_for_draft(doc, rev)) + ref_rev_file_path = Path(settings.BIBXML_BASE_PATH) / "bibxml-ids" / f"reference.I-D.{doc.name}-{rev}.xml" + try: + existing_bibxml = ref_rev_file_path.read_text(encoding="utf8") + except IOError: + existing_bibxml = "" + if normalized_bibxml.strip() != existing_bibxml.strip(): + log.log(f"Writing {ref_rev_file_path}") + ref_rev_file_path.write_text(normalized_bibxml, encoding="utf8") + + +def ensure_draft_bibxml_path_exists(): + (Path(settings.BIBXML_BASE_PATH) / "bibxml-ids").mkdir(exist_ok=True) diff --git a/ietf/doc/utils_charter.py b/ietf/doc/utils_charter.py index 7d2001e4d7..b29d1e303c 100644 --- a/ietf/doc/utils_charter.py +++ b/ietf/doc/utils_charter.py @@ -92,11 +92,31 @@ def change_group_state_after_charter_approval(group, by): def fix_charter_revision_after_approval(charter, by): # according to spec, 00-02 becomes 01, so copy file and record new revision try: - old = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.name, charter.rev)) - new = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.name, next_approved_revision(charter.rev))) + old = os.path.join( + charter.get_file_path(), "%s-%s.txt" % (charter.name, charter.rev) + ) + new = os.path.join( + charter.get_file_path(), + "%s-%s.txt" % (charter.name, next_approved_revision(charter.rev)), + ) shutil.copy(old, new) except IOError: log("There was an error copying %s to %s" % (old, new)) + # Also provide a copy to the legacy ftp source directory, which is served by rsync + # This replaces the hardlink copy that ghostlink has made in the past + # Still using a hardlink as long as these are on the same filesystem. + # Staying with os.path vs pathlib.Path until we get to python>=3.10. + charter_dir = os.path.join(settings.FTP_DIR, "charter") + ftp_filepath = os.path.join( + charter_dir, "%s-%s.txt" % (charter.name, next_approved_revision(charter.rev)) + ) + try: + os.link(new, ftp_filepath) + except IOError: + log( + "There was an error creating a harlink at %s pointing to %s" + % (ftp_filepath, new) + ) events = [] e = NewRevisionDocEvent(doc=charter, by=by, type="new_revision") @@ -108,6 +128,7 @@ def fix_charter_revision_after_approval(charter, by): charter.rev = e.rev charter.save_with_history(events) + def historic_milestones_for_charter(charter, rev): """Return GroupMilestone/GroupMilestoneHistory objects for charter document at rev by looking through the history.""" diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 9b0ccdcead..02b55249d6 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -38,6 +38,7 @@ from ietf.message.utils import infer_message from ietf.name.models import BallotPositionName, DocTypeName from ietf.person.models import Person +from ietf.utils.fields import ModelMultipleChoiceField from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.utils.decorators import require_api_key from ietf.utils.response import permission_denied @@ -686,7 +687,8 @@ def ballot_writeupnotes(request, name): dict(doc=doc, back_url=doc.get_absolute_url(), ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")), - ballot_issue_danger=bool(prev_state.slug in ['ad-eval', 'lc']), + warn_lc = not doc.docevent_set.filter(lastcalldocevent__expires__date__lt=date_today(DEADLINE_TZINFO)).exists(), + warn_unexpected_state= prev_state if bool(prev_state.slug in ['watching', 'ad-eval', 'lc']) else None, ballot_writeup_form=form, need_intended_status=need_intended_status, )) @@ -931,7 +933,7 @@ def approve_ballot(request, name): class ApproveDownrefsForm(forms.Form): - checkboxes = forms.ModelMultipleChoiceField( + checkboxes = ModelMultipleChoiceField( widget = forms.CheckboxSelectMultiple, queryset = RelatedDocument.objects.none(), ) diff --git a/ietf/doc/views_charter.py b/ietf/doc/views_charter.py index 9596970f86..f8748d2126 100644 --- a/ietf/doc/views_charter.py +++ b/ietf/doc/views_charter.py @@ -4,6 +4,7 @@ import datetime import json +import os import textwrap from pathlib import Path @@ -36,13 +37,13 @@ from ietf.doc.mails import email_state_changed, email_charter_internal_review from ietf.group.mails import email_admin_re_charter from ietf.group.models import Group, ChangeStateGroupEvent, MilestoneGroupEvent -from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_all_groups_of_type -from ietf.group.views import fill_in_charter_info +from ietf.group.utils import save_group_in_history, save_milestone_in_history, can_manage_all_groups_of_type, \ + fill_in_charter_info from ietf.ietfauth.utils import has_role, role_required from ietf.name.models import GroupStateName from ietf.person.models import Person from ietf.utils.history import find_history_active_at -from ietf.utils.log import assertion +from ietf.utils.log import assertion, log from ietf.utils.mail import send_mail_preformatted from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.response import permission_denied @@ -443,6 +444,18 @@ def submit(request, name, option=None): destination.write(form.cleaned_data["txt"]) else: destination.write(form.cleaned_data["content"]) + # Also provide a copy to the legacy ftp source directory, which is served by rsync + # This replaces the hardlink copy that ghostlink has made in the past + # Still using a hardlink as long as these are on the same filesystem. + ftp_filename = Path(settings.FTP_DIR) / "charter" / charter_filename.name + try: + os.link(charter_filename, ftp_filename) # os.link until we are on python>=3.10 + except IOError: + log( + "There was an error creating a hardlink at %s pointing to %s" + % (ftp_filename, charter_filename) + ) + if option in ["initcharter", "recharter"] and charter.ad == None: charter.ad = getattr(group.ad_role(), "person", None) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 551ec0cc5b..42898d2098 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -35,13 +35,13 @@ import glob -import io import json import os import re from pathlib import Path +from django.core.cache import caches from django.db.models import Max from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404, redirect @@ -49,6 +49,7 @@ from django.urls import reverse as urlreverse from django.conf import settings from django import forms +from django.contrib.auth.decorators import login_required from django.contrib.staticfiles import finders import debug # pyflakes:ignore @@ -58,13 +59,13 @@ IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor, RelatedDocument, RelatedDocHistory) from ietf.doc.utils import (augment_events_with_revision, - can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id, + can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id, investigate_fragment, needed_ballot_positions, nice_consensus, update_telechat, has_same_ballot, get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus, add_events_message_info, get_unicode_document_content, augment_docs_and_person_with_person_info, irsg_needed_ballot_positions, add_action_holder_change_event, build_file_urls, update_documentauthors, fuzzy_find_documents, - bibxml_for_draft) + bibxml_for_draft, get_doc_email_aliases) from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible from ietf.group.models import Role, Group from ietf.group.utils import can_manage_all_groups_of_type, can_manage_materials, group_features_role_filter @@ -72,7 +73,7 @@ role_required, is_individual_draft_author, can_request_rfc_publication) from ietf.name.models import StreamName, BallotPositionName from ietf.utils.history import find_history_active_at -from ietf.doc.forms import TelechatForm, NotifyForm, ActionHoldersForm, DocAuthorForm, DocAuthorChangeBasisForm +from ietf.doc.forms import InvestigateForm, TelechatForm, NotifyForm, ActionHoldersForm, DocAuthorForm, DocAuthorChangeBasisForm from ietf.doc.mails import email_comment, email_remind_action_holders from ietf.mailtrigger.utils import gather_relevant_expansions from ietf.meeting.models import Session, SessionPresentation @@ -1071,32 +1072,6 @@ def document_pdfized(request, name, rev=None, ext=None): else: raise Http404 -def check_doc_email_aliases(): - pattern = re.compile(r'^expand-(.*?)(\..*?)?@.*? +(.*)$') - good_count = 0 - tot_count = 0 - with io.open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file: - for line in virtual_file.readlines(): - m = pattern.match(line) - tot_count += 1 - if m: - good_count += 1 - if good_count > 50 and tot_count < 3*good_count: - return True - return False - -def get_doc_email_aliases(name): - if name: - pattern = re.compile(r'^expand-(%s)(\..*?)?@.*? +(.*)$'%name) - else: - pattern = re.compile(r'^expand-(.*?)(\..*?)?@.*? +(.*)$') - aliases = [] - with io.open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file: - for line in virtual_file.readlines(): - m = pattern.match(line) - if m: - aliases.append({'doc_name':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)}) - return aliases def document_email(request,name): doc = get_object_or_404(Document, name=name) @@ -1547,21 +1522,12 @@ def document_ballot_content(request, doc, ballot_id, editable=True): def document_ballot(request, name, ballot_id=None): doc = get_object_or_404(Document, name=name) - all_ballots = list(BallotDocEvent.objects.filter(doc=doc, type="created_ballot").order_by("time")) - if not ballot_id: - if all_ballots: - ballot = all_ballots[-1] - else: - raise Http404("Ballot not found for: %s" % name) - ballot_id = ballot.id + ballots = BallotDocEvent.objects.filter(doc=doc, type="created_ballot").order_by("time") + if ballot_id is not None: + ballot = ballots.filter(id=ballot_id).first() else: - ballot_id = int(ballot_id) - for b in all_ballots: - if b.id == ballot_id: - ballot = b - break - - if not ballot_id or not ballot: + ballot = ballots.last() + if not ballot: raise Http404("Ballot not found for: %s" % name) if ballot.ballot_type.slug == "irsg-approve": @@ -1571,14 +1537,13 @@ def document_ballot(request, name, ballot_id=None): top = render_document_top(request, doc, ballot_tab, name) - c = document_ballot_content(request, doc, ballot_id, editable=True) + c = document_ballot_content(request, doc, ballot.id, editable=True) request.session['ballot_edit_return_point'] = request.path_info return render(request, "doc/document_ballot.html", dict(doc=doc, top=top, ballot_content=c, - # ballot_type_slug=ballot.ballot_type.slug, )) def document_irsg_ballot(request, name, ballot_id=None): @@ -2031,16 +1996,26 @@ def remind_action_holders(request, name): ) -def email_aliases(request,name=''): - doc = get_object_or_404(Document, name=name) if name else None - if not name: - # require login for the overview page, but not for the - # document-specific pages - if not request.user.is_authenticated: - return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) - aliases = get_doc_email_aliases(name) - - return render(request,'doc/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'doc':doc}) +@login_required +def email_aliases(request): + """List of all email aliases + + This is currently slow except when cached + """ + slowcache = caches["slowpages"] + cache_key = "emailaliasesview" + aliases = slowcache.get(cache_key) + if not aliases: + aliases = get_doc_email_aliases() # gets all aliases + slowcache.set(cache_key, aliases, 3600) + return render( + request, + "doc/email_aliases.html", + { + "aliases": aliases, + "ietf_domain": settings.IETF_DOMAIN, + }, + ) class VersionForm(forms.Form): @@ -2264,3 +2239,16 @@ def idnits2_state(request, name, rev=None): content_type="text/plain;charset=utf-8", ) +@role_required("Secretariat") +def investigate(request): + results = None + if request.method == "POST": + form = InvestigateForm(request.POST) + if form.is_valid(): + name_fragment = form.cleaned_data["name_fragment"] + results = investigate_fragment(name_fragment) + else: + form = InvestigateForm() + return render( + request, "doc/investigate.html", context=dict(form=form, results=results) + ) diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index ea30e7bd2d..30175491da 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -52,6 +52,7 @@ from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils import log +from ietf.utils.fields import ModelMultipleChoiceField from ietf.utils.response import permission_denied from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO @@ -390,9 +391,9 @@ def replaces(request, name): )) class SuggestedReplacesForm(forms.Form): - replaces = forms.ModelMultipleChoiceField(queryset=Document.objects.all(), - label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple, - help_text="Select only the documents that are replaced by this document") + replaces = ModelMultipleChoiceField(queryset=Document.objects.all(), + label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple, + help_text="Select only the documents that are replaced by this document") comment = forms.CharField(label="Optional comment", widget=forms.Textarea, required=False, strip=False) def __init__(self, suggested, *args, **kwargs): @@ -831,6 +832,9 @@ def restore_draft_file(request, draft): log.log("Resurrecting %s. Moving files:" % draft.name) for file in files: try: + # ghostlinkd would keep this in the combined all archive since it would + # be sourced from a different place. But when ghostlinkd is removed, nothing + # new is needed here - the file will already exist in the combined archive shutil.move(file, settings.INTERNET_DRAFT_PATH) log.log(" Moved file %s to %s" % (file, settings.INTERNET_DRAFT_PATH)) except shutil.Error as ex: @@ -1598,7 +1602,7 @@ class ChangeStreamStateForm(forms.Form): new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State' ) weeks = forms.IntegerField(label='Expected weeks in state',required=False) comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history.", strip=False) - tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False) + tags = ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False) def __init__(self, *args, **kwargs): doc = kwargs.pop("doc") diff --git a/ietf/doc/views_help.py b/ietf/doc/views_help.py index e8e63ed8b7..34d29aaccb 100644 --- a/ietf/doc/views_help.py +++ b/ietf/doc/views_help.py @@ -9,6 +9,18 @@ from ietf.name.models import DocRelationshipName, DocTagName from ietf.doc.utils import get_tags_for_stream_id +def state_index(request): + types = StateType.objects.all() + names = [ type.slug for type in types ] + for type in types: + if "-" in type.slug and type.slug.split('-',1)[0] in names: + type.stategroups = None + else: + groups = StateType.objects.filter(slug__startswith=type.slug) + type.stategroups = [ g.slug[len(type.slug)+1:] for g in groups if not g == type ] or "" + + return render(request, 'doc/state_index.html', {"types": types}) + def state_help(request, type=None): slug, title = { "draft-iesg": ("draft-iesg", "IESG States for Internet-Drafts"), @@ -26,7 +38,30 @@ def state_help(request, type=None): "status-change": ("statchg", "RFC Status Change States"), "bofreq": ("bofreq", "BOF Request States"), "procmaterials": ("procmaterials", "Proceedings Materials States"), - "statement": {"statement", "Statement States"} + "statement": ("statement", "Statement States"), + "slides": ("slides", "Slides States"), + "minutes": ("minutes", "Minutes States"), + "liai-att": ("liai-att", "Liaison Attachment States"), + "recording": ("recording", "Recording States"), + "bluesheets": ("bluesheets", "Bluesheets States"), + "reuse_policy": ("reuse_policy", "Reuse Policy States"), + "review": ("review", "Review States"), + "liaison": ("liaison", "Liaison States"), + "shepwrit": ("shepwrit", "Shapherd Writeup States"), + "bofreq": ("bofreq", "BOF Request States"), + "procmaterials": ("procmaterials", "Proceedings Materials States"), + "chatlog": ("chatlog", "Chat Log States"), + "polls": ("polls", "Polls States"), + "statement": ("statement", "Statement States"), + "rfc": ("rfc", "RFC States"), + "bcp": ("bcp", "BCP States"), + "std": ("std", "STD States"), + "fyi": ("fyi", "FYI States"), + "narrativeminutes": ("narrativeminutes", "Narrative Minutes States"), + "draft": ("draft", "Draft States"), + "statchg": ("statchg", "Status Change States"), + "agenda": ("agenda", "Agenda States"), + "conflrev": ("conflrev", "Conflict Review States") }.get(type, (None, None)) state_type = get_object_or_404(StateType, slug=slug) diff --git a/ietf/doc/views_review.py b/ietf/doc/views_review.py index f1f9d7be4e..bb9e56742d 100644 --- a/ietf/doc/views_review.py +++ b/ietf/doc/views_review.py @@ -2,11 +2,11 @@ # -*- coding: utf-8 -*- -import io import itertools import json import os import datetime +from pathlib import Path import requests import email.utils @@ -52,7 +52,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.mail import send_mail_message from ietf.mailtrigger.utils import gather_address_lists -from ietf.utils.fields import MultiEmailField +from ietf.utils.fields import ModelMultipleChoiceField, MultiEmailField from ietf.utils.http import is_ajax from ietf.utils.response import permission_denied from ietf.utils.timezone import date_today, DEADLINE_TZINFO @@ -68,7 +68,7 @@ def clean_doc_revision(doc, rev): return rev class RequestReviewForm(forms.ModelForm): - team = forms.ModelMultipleChoiceField(queryset=Group.objects.all(), widget=forms.CheckboxSelectMultiple) + team = ModelMultipleChoiceField(queryset=Group.objects.all(), widget=forms.CheckboxSelectMultiple) deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" }) class Meta: @@ -803,9 +803,13 @@ def complete_review(request, name, assignment_id=None, acronym=None): else: content = form.cleaned_data['review_content'] - filename = os.path.join(review.get_file_path(), '{}.txt'.format(review.name)) - with io.open(filename, 'w', encoding='utf-8') as destination: - destination.write(content) + review_path = Path(review.get_file_path()) / f"{review.name}.txt" + review_path.write_text(content) + review_ftp_path = Path(settings.FTP_DIR) / "review" / review_path.name + # See https://github.com/ietf-tools/datatracker/issues/6941 - when that's + # addressed, making this link should not be conditional + if not review_ftp_path.exists(): + os.link(review_path, review_ftp_path) # switch this to Path.hardlink when python>=3.10 is available completion_datetime = timezone.now() if "completion_date" in form.cleaned_data: @@ -1053,7 +1057,11 @@ def edit_deadline(request, name, request_id): if form.is_valid(): if form.cleaned_data['deadline'] != old_deadline: form.save() - subject = "Deadline changed: {} {} review of {}-{}".format(review_req.team.acronym.capitalize(),review_req.type.name.lower(), review_req.doc.name, review_req.requested_rev) + subject = f"Deadline changed: {review_req.team.acronym.capitalize()} {review_req.type.name.lower()} review of {review_req.doc.name}" + if review_req.requested_rev: + subject += f"-{review_req.requested_rev}" + descr = "Deadine changed from {} to {}".format(old_deadline, review_req.deadline) + update_change_reason(review_req, descr) msg = render_to_string("review/deadline_changed.txt", { "review_req": review_req, "old_deadline": old_deadline, diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 422e38f7dd..2ef4ee83e6 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -69,6 +69,7 @@ from ietf.person.models import Person from ietf.person.utils import get_active_ads from ietf.utils.draft_search import normalize_draftname +from ietf.utils.fields import ModelMultipleChoiceField from ietf.utils.log import log from ietf.doc.utils_search import prepare_document_table, doc_type, doc_state, doc_type_name, AD_WORKLOAD from ietf.ietfauth.utils import has_role @@ -100,7 +101,7 @@ class SearchForm(forms.Form): ("ad", "AD"), ("-ad", "AD (desc)"), ), required=False, widget=forms.HiddenInput) - doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug__in=('draft', 'rfc', 'bcp', 'std', 'fyi', 'liai-att')).order_by('name'), required=False) + doctypes = ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug__in=('draft', 'rfc', 'bcp', 'std', 'fyi', 'liai-att')).order_by('name'), required=False) def __init__(self, *args, **kwargs): super(SearchForm, self).__init__(*args, **kwargs) diff --git a/ietf/group/management/commands/generate_group_aliases.py b/ietf/group/management/commands/generate_group_aliases.py deleted file mode 100755 index 630f35c441..0000000000 --- a/ietf/group/management/commands/generate_group_aliases.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright The IETF Trust 2012-2021, All Rights Reserved -# -*- coding: utf-8 -*- - -# This was written as a script by Markus Stenberg . -# It was turned into a management command by Russ Housley . - -import datetime -import io -import os -import shutil -import stat -import time - -from tempfile import mkstemp - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.utils import timezone - -import debug # pyflakes:ignore - -from ietf.group.models import Group -from ietf.group.utils import get_group_ad_emails, get_group_role_emails, get_child_group_role_emails -from ietf.name.models import GroupTypeName -from ietf.utils.aliases import dump_sublist - -DEFAULT_YEARS = 5 -ACTIVE_STATES=['active', 'bof', 'proposed'] -GROUP_TYPES=['wg', 'rg', 'rag', 'dir', 'team', 'review', 'program', 'rfcedtyp', 'edappr', 'edwg'] # This should become groupfeature driven... -NO_AD_GROUP_TYPES=['rg', 'rag', 'team', 'program', 'rfcedtyp', 'edappr', 'edwg'] -IETF_DOMAIN=['ietf.org', ] -IRTF_DOMAIN=['irtf.org', ] -IAB_DOMAIN=['iab.org', ] - -class Command(BaseCommand): - help = ('Generate the group-aliases and group-virtual files for Internet-Draft ' - 'mail aliases, placing them in the file configured in ' - 'settings.GROUP_ALIASES_PATH and settings.GROUP_VIRTUAL_PATH, ' - 'respectively. The generation includes aliases for groups that ' - 'have seen activity in the last %s years.' % (DEFAULT_YEARS)) - - def handle(self, *args, **options): - show_since = timezone.now() - datetime.timedelta(DEFAULT_YEARS*365) - - date = time.strftime("%Y-%m-%d_%H:%M:%S") - signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date) - - ahandle, aname = mkstemp() - os.close(ahandle) - afile = io.open(aname,"w") - - vhandle, vname = mkstemp() - os.close(vhandle) - vfile = io.open(vname,"w") - - afile.write(signature) - vfile.write(signature) - vfile.write("%s anything\n" % settings.GROUP_VIRTUAL_DOMAIN) - - # Loop through each group type and build -ads and -chairs entries - for g in GROUP_TYPES: - domains = [] - domains += IETF_DOMAIN - if g in ('rg', 'rag'): - domains += IRTF_DOMAIN - if g == 'program': - domains += IAB_DOMAIN - - entries = Group.objects.filter(type=g).all() - active_entries = entries.filter(state__in=ACTIVE_STATES) - inactive_recent_entries = entries.exclude(state__in=ACTIVE_STATES).filter(time__gte=show_since) - interesting_entries = active_entries | inactive_recent_entries - - for e in interesting_entries.distinct().iterator(): - name = e.acronym - - # Research groups, teams, and programs do not have -ads lists - if not g in NO_AD_GROUP_TYPES: - dump_sublist(afile, vfile, name+'-ads', domains, settings.GROUP_VIRTUAL_DOMAIN, get_group_ad_emails(e)) - # All group types have -chairs lists - dump_sublist(afile, vfile, name+'-chairs', domains, settings.GROUP_VIRTUAL_DOMAIN, get_group_role_emails(e, ['chair', 'secr'])) - - # The area lists include every chair in active working groups in the area - areas = Group.objects.filter(type='area').all() - active_areas = areas.filter(state__in=ACTIVE_STATES) - for area in active_areas: - name = area.acronym - area_ad_emails = get_group_role_emails(area, ['pre-ad', 'ad', 'chair']) - dump_sublist(afile, vfile, name+'-ads', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, area_ad_emails) - dump_sublist(afile, vfile, name+'-chairs', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, (get_child_group_role_emails(area, ['chair', 'secr']) | area_ad_emails)) - - # Other groups with chairs that require Internet-Draft submission approval - gtypes = GroupTypeName.objects.values_list('slug', flat=True) - special_groups = Group.objects.filter(type__features__req_subm_approval=True, acronym__in=gtypes, state='active') - for group in special_groups: - dump_sublist(afile, vfile, group.acronym+'-chairs', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, get_group_role_emails(group, ['chair', 'delegate'])) - - afile.close() - vfile.close() - - os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH) - os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH) - - shutil.move(aname, settings.GROUP_ALIASES_PATH) - shutil.move(vname, settings.GROUP_VIRTUAL_PATH) diff --git a/ietf/group/management/commands/import_iesg_appeals.py b/ietf/group/management/commands/import_iesg_appeals.py deleted file mode 100644 index 1c4ebe3f83..0000000000 --- a/ietf/group/management/commands/import_iesg_appeals.py +++ /dev/null @@ -1,294 +0,0 @@ -# Copyright The IETF Trust 2023, All Rights Reserved - -import csv -import datetime -import re -import shutil -import subprocess -import tempfile - -from pathlib import Path -import dateutil - -from django.conf import settings -from django.core.management import BaseCommand - -from ietf.group.models import Appeal, AppealArtifact - - -class Command(BaseCommand): - help = "Performs a one-time import of IESG appeals" - - def handle(self, *args, **options): - old_appeals_root = ( - "/a/www/www6/iesg/appeal" - if settings.SERVER_MODE == "production" - else "/assets/www6/iesg/appeal" - ) - tmpdir = tempfile.mkdtemp() - process = subprocess.Popen( - ["git", "clone", "https://github.com/kesara/iesg-scraper.git", tmpdir], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - sub_stdout, sub_stderr = process.communicate() - if not (Path(tmpdir) / "iesg_appeals" / "anderson-2006-03-08.md").exists(): - self.stdout.write( - "Git clone of the iesg-scraper directory did not go as expected" - ) - self.stdout.write("stdout:", sub_stdout) - self.stdout.write("stderr:", sub_stderr) - self.stdout.write(f"Clean up {tmpdir} manually") - exit(-1) - titles = [ - "Appeal: IESG Statement on Guidance on In-Person and Online Interim Meetings (John Klensin, 2023-08-15)", - "Appeal of current Guidance on in-Person and Online meetings (Ted Hardie, Alan Frindell, 2023-07-19)", - "Appeal re: URI Scheme Application and draft-mcsweeney-drop-scheme (Tim McSweeney, 2020-07-08)", - "Appeal to the IESG re WGLC of draft-ietf-spring-srv6-network-programming (Fernando Gont, Andrew Alston, and Sander Steffann, 2020-04-22)", - "Appeal re Protocol Action: 'URI Design and Ownership' to Best \nCurrent Practice (draft-nottingham-rfc7320bis-03.txt) (John Klensin; 2020-02-04)", - "Appeal of IESG Conflict Review process and decision on draft-mavrogiannopoulos-pkcs8-validated-parameters-02 (John Klensin; 2018-07-07)", - "Appeal of IESG decision to defer action and request that ISE publish draft-klensin-dns-function-considerations (John Klensin; 2017-11-29)", - 'Appeal to the IESG concerning its approval of the "draft-ietf-ianaplan-icg-response" (PDF file) (JFC Morfin; 2015-03-11)', - "Appeal re tzdist mailing list moderation (Tobias Conradi; 2014-08-28) / Withdrawn by Submitter", - "Appeal re draft-masotta-tftpexts-windowsize-opt (Patrick Masotta; 2013-11-14)", - "Appeal re draft-ietf-manet-nhdp-sec-threats (Abdussalam Baryun; 2013-06-19)", - "Appeal of decision to advance RFC6376 (Douglas Otis; 2013-05-30)", - "Appeal to the IESG in regards to RFC 6852 (PDF file) (JFC Morfin; 2013-04-05)", - "Appeal to the IESG concerning the approbation of the IDNA2008 document set (PDF file) (JFC Morfin; 2010-03-10)", - "Authentication-Results Header Field Appeal (Douglas Otis, David Rand; 2009-02-16) / Withdrawn by Submitter", - "Appeal to the IAB of IESG rejection of Appeal to Last Call draft-ietf-grow-anycast (Dean Anderson; 2008-11-14)", - "Appeal to the IESG Concerning the Way At Large Internet Lead Users Are Not Permitted To Adequately Contribute to the IETF Deliverables (JFC Morfin; 2008-09-10)", - "Appeal over suspension of posting rights for Todd Glassey (Todd Glassey; 2008-07-28)", - "Appeal against IESG blocking DISCUSS on draft-klensin-rfc2821bis (John C Klensin; 2008-06-13)", - "Appeal: Continued Abuse of Process by IPR-WG Chair (Dean Anderson; 2007-12-26)", - "Appeal to the IESG from Todd Glassey (Todd Glassey; 2007-11-26)", - "Appeal Against the Removal of the Co-Chairs of the GEOPRIV Working Group (PDF file) (Randall Gellens, Allison Mankin, and Andrew Newton; 2007-06-22)", - "Appeal concerning the WG-LTRU rechartering (JFC Morfin; 2006-10-24)", - "Appeal against decision within July 10 IESG appeal dismissal (JFC Morfin; 2006-09-09)", - "Appeal: Mandatory to implement HTTP authentication mechanism in the Atom Publishing Protocol (Robert Sayre; 2006-08-29)", - "Appeal Against IESG Decisions Regarding the draft-ietf-ltru-matching (PDF file) (JFC Morfin; 2006-08-16)", - "Amended Appeal Re: grow: Last Call: 'Operation of Anycast Services' to BCP (draft-ietf-grow-anycast) (Dean Anderson; 2006-06-14)", - "Appeal Against an IESG Decision Denying Me IANA Language Registration Process by way of PR-Action (PDF file) (JFC Morfin; 2006-05-17)", - "Appeal to the IESG of PR-Action against Dean Anderson (Dean Anderson; 2006-03-08)", - "Appeal to IESG against AD decision: one must clear the confusion opposing the RFC 3066 Bis consensus (JFC Morfin; 2006-02-20)", - "Appeal to the IESG of an IESG decision (JFC Morfin; 2006-02-17)", - "Appeal to the IESG in reference to the ietf-languages@alvestrand.no mailing list (JFC Morfin; 2006-02-07)", - "Appeal to the IESG against an IESG decision concerning RFC 3066 Bis Draft (JFC Morfin; 2006-01-14)", - "Appeal over a key change in a poor RFC 3066 bis example (JFC Morfin; 2005-10-19)", - "Additional appeal against publication of draft-lyon-senderid-* in regards to its recommended use of Resent- header fields in the way that is inconsistant with RFC2822(William Leibzon; 2005-08-29)", - "Appeal: Publication of draft-lyon-senderid-core-01 in conflict with referenced draft-schlitt-spf-classic-02 (Julian Mehnle; 2005-08-25)", - 'Appeal of decision to standardize "Mapping Between the Multimedia Messaging Service (MMS) and Internet Mail" (John C Klensin; 2005-06-10)', - "Appeal regarding IESG decision on the GROW WG (David Meyer; 2003-11-15)", - "Appeal: Official notice of appeal on suspension rights (Todd Glassey; 2003-08-06)", - "Appeal: AD response to Site-Local Appeal (Tony Hain; 2003-07-31)", - "Appeal against IESG decision for draft-chiba-radius-dynamic-authorization-05.txt (Glen Zorn; 2003-01-15)", - "Appeal Against moving draft-ietf-ipngwg-addr-arch-v3 to Draft Standard (Robert Elz; 2002-11-05)", - ] - date_re = re.compile(r"\d{4}-\d{2}-\d{2}") - dates = [ - datetime.datetime.strptime(date_re.search(t).group(), "%Y-%m-%d").date() - for t in titles - ] - - parts = [ - ["klensin-2023-08-15.txt", "response-to-klensin-2023-08-15.txt"], - [ - "hardie-frindell-2023-07-19.txt", - "response-to-hardie-frindell-2023-07-19.txt", - ], - ["mcsweeney-2020-07-08.txt", "response-to-mcsweeney-2020-07-08.pdf"], - ["gont-2020-04-22.txt", "response-to-gont-2020-06-02.txt"], - ["klensin-2020-02-04.txt", "response-to-klensin-2020-02-04.txt"], - ["klensin-2018-07-07.txt", "response-to-klensin-2018-07-07.txt"], - ["klensin-2017-11-29.txt", "response-to-klensin-2017-11-29.md"], - ["morfin-2015-03-11.pdf", "response-to-morfin-2015-03-11.md"], - ["conradi-2014-08-28.txt"], - ["masotta-2013-11-14.txt", "response-to-masotta-2013-11-14.md"], - ["baryun-2013-06-19.txt", "response-to-baryun-2013-06-19.md"], - ["otis-2013-05-30.txt", "response-to-otis-2013-05-30.md"], - ["morfin-2013-04-05.pdf", "response-to-morfin-2013-04-05.md"], - ["morfin-2010-03-10.pdf", "response-to-morfin-2010-03-10.txt"], - ["otis-2009-02-16.txt"], - ["anderson-2008-11-14.md", "response-to-anderson-2008-11-14.txt"], - ["morfin-2008-09-10.txt", "response-to-morfin-2008-09-10.txt"], - ["glassey-2008-07-28.txt", "response-to-glassey-2008-07-28.txt"], - ["klensin-2008-06-13.txt", "response-to-klensin-2008-06-13.txt"], - ["anderson-2007-12-26.txt", "response-to-anderson-2007-12-26.txt"], - ["glassey-2007-11-26.txt", "response-to-glassey-2007-11-26.txt"], - ["gellens-2007-06-22.pdf", "response-to-gellens-2007-06-22.txt"], - ["morfin-2006-10-24.txt", "response-to-morfin-2006-10-24.txt"], - ["morfin-2006-09-09.txt", "response-to-morfin-2006-09-09.txt"], - ["sayre-2006-08-29.txt", "response-to-sayre-2006-08-29.txt"], - [ - "morfin-2006-08-16.pdf", - "response-to-morfin-2006-08-17.txt", - "response-to-morfin-2006-08-17-part2.txt", - ], - ["anderson-2006-06-13.txt", "response-to-anderson-2006-06-14.txt"], - ["morfin-2006-05-17.pdf", "response-to-morfin-2006-05-17.txt"], - ["anderson-2006-03-08.md", "response-to-anderson-2006-03-08.txt"], - ["morfin-2006-02-20.txt", "response-to-morfin-2006-02-20.txt"], - ["morfin-2006-02-17.txt", "response-to-morfin-2006-02-17.txt"], - ["morfin-2006-02-07.txt", "response-to-morfin-2006-02-07.txt"], - ["morfin-2006-01-14.txt", "response-to-morfin-2006-01-14.txt"], - ["morfin-2005-10-19.txt", "response-to-morfin-2005-10-19.txt"], - ["leibzon-2005-08-29.txt", "response-to-leibzon-2005-08-29.txt"], - ["mehnle-2005-08-25.txt", "response-to-mehnle-2005-08-25.txt"], - ["klensin-2005-06-10.txt", "response-to-klensin-2005-06-10.txt"], - ["meyer-2003-11-15.txt", "response-to-meyer-2003-11-15.txt"], - ["glassey-2003-08-06.txt", "response-to-glassey-2003-08-06.txt"], - ["hain-2003-07-31.txt", "response-to-hain-2003-07-31.txt"], - ["zorn-2003-01-15.txt", "response-to-zorn-2003-01-15.txt"], - ["elz-2002-11-05.txt", "response-to-elz-2002-11-05.txt"], - ] - - assert len(titles) == len(dates) - assert len(titles) == len(parts) - - part_times = dict() - part_times["klensin-2023-08-15.txt"] = "2023-08-15 15:03:55 -0400" - part_times["response-to-klensin-2023-08-15.txt"] = "2023-08-24 18:54:13 +0300" - part_times["hardie-frindell-2023-07-19.txt"] = "2023-07-19 07:17:16PDT" - part_times["response-to-hardie-frindell-2023-07-19.txt"] = ( - "2023-08-15 11:58:26PDT" - ) - part_times["mcsweeney-2020-07-08.txt"] = "2020-07-08 14:45:00 -0400" - part_times["response-to-mcsweeney-2020-07-08.pdf"] = "2020-07-28 12:54:04 -0000" - part_times["gont-2020-04-22.txt"] = "2020-04-22 22:26:20 -0400" - part_times["response-to-gont-2020-06-02.txt"] = "2020-06-02 20:44:29 -0400" - part_times["klensin-2020-02-04.txt"] = "2020-02-04 13:54:46 -0500" - # part_times["response-to-klensin-2020-02-04.txt"]="2020-03-24 11:49:31EDT" - part_times["response-to-klensin-2020-02-04.txt"] = "2020-03-24 11:49:31 -0400" - part_times["klensin-2018-07-07.txt"] = "2018-07-07 12:40:43PDT" - # part_times["response-to-klensin-2018-07-07.txt"]="2018-08-16 10:46:45EDT" - part_times["response-to-klensin-2018-07-07.txt"] = "2018-08-16 10:46:45 -0400" - part_times["klensin-2017-11-29.txt"] = "2017-11-29 09:35:02 -0500" - part_times["response-to-klensin-2017-11-29.md"] = "2017-11-30 11:33:04 -0500" - part_times["morfin-2015-03-11.pdf"] = "2015-03-11 18:03:44 -0000" - part_times["response-to-morfin-2015-03-11.md"] = "2015-04-16 15:18:09 -0000" - part_times["conradi-2014-08-28.txt"] = "2014-08-28 22:28:06 +0300" - part_times["masotta-2013-11-14.txt"] = "2013-11-14 15:35:19 +0200" - part_times["response-to-masotta-2013-11-14.md"] = "2014-01-27 07:39:32 -0800" - part_times["baryun-2013-06-19.txt"] = "2013-06-19 06:29:51PDT" - part_times["response-to-baryun-2013-06-19.md"] = "2013-07-02 15:24:42 -0700" - part_times["otis-2013-05-30.txt"] = "2013-05-30 19:35:18 +0000" - part_times["response-to-otis-2013-05-30.md"] = "2013-06-27 11:56:48 -0700" - part_times["morfin-2013-04-05.pdf"] = "2013-04-05 17:31:19 -0700" - part_times["response-to-morfin-2013-04-05.md"] = "2013-04-17 08:17:29 -0700" - part_times["morfin-2010-03-10.pdf"] = "2010-03-10 21:40:58 +0100" - part_times["response-to-morfin-2010-03-10.txt"] = "2010-04-07 14:26:06 -0700" - part_times["otis-2009-02-16.txt"] = "2009-02-16 15:47:15 -0800" - part_times["anderson-2008-11-14.md"] = "2008-11-14 00:16:58 -0500" - part_times["response-to-anderson-2008-11-14.txt"] = "2008-12-15 11:00:02 -0800" - part_times["morfin-2008-09-10.txt"] = "2008-09-10 04:10:13 +0200" - part_times["response-to-morfin-2008-09-10.txt"] = "2008-09-28 10:00:01PDT" - part_times["glassey-2008-07-28.txt"] = "2008-07-28 08:34:52 -0700" - part_times["response-to-glassey-2008-07-28.txt"] = "2008-09-02 11:00:01PDT" - part_times["klensin-2008-06-13.txt"] = "2008-06-13 21:14:38 -0400" - part_times["response-to-klensin-2008-06-13.txt"] = "2008-07-07 10:00:01 PDT" - # part_times["anderson-2007-12-26.txt"]="2007-12-26 17:19:34EST" - part_times["anderson-2007-12-26.txt"] = "2007-12-26 17:19:34 -0500" - part_times["response-to-anderson-2007-12-26.txt"] = "2008-01-15 17:21:05 -0500" - part_times["glassey-2007-11-26.txt"] = "2007-11-26 08:13:22 -0800" - part_times["response-to-glassey-2007-11-26.txt"] = "2008-01-23 17:38:43 -0500" - part_times["gellens-2007-06-22.pdf"] = "2007-06-22 21:45:41 -0400" - part_times["response-to-gellens-2007-06-22.txt"] = "2007-09-20 14:01:27 -0400" - part_times["morfin-2006-10-24.txt"] = "2006-10-24 05:03:17 +0200" - part_times["response-to-morfin-2006-10-24.txt"] = "2006-11-07 12:56:02 -0500" - part_times["morfin-2006-09-09.txt"] = "2006-09-09 02:54:55 +0200" - part_times["response-to-morfin-2006-09-09.txt"] = "2006-09-15 12:56:31 -0400" - part_times["sayre-2006-08-29.txt"] = "2006-08-29 17:05:03 -0400" - part_times["response-to-sayre-2006-08-29.txt"] = "2006-10-16 13:07:18 -0400" - part_times["morfin-2006-08-16.pdf"] = "2006-08-16 18:28:19 -0400" - part_times["response-to-morfin-2006-08-17.txt"] = "2006-08-22 12:05:42 -0400" - part_times["response-to-morfin-2006-08-17-part2.txt"] = ( - "2006-11-07 13:00:58 -0500" - ) - # part_times["anderson-2006-06-13.txt"]="2006-06-13 21:51:18EDT" - part_times["anderson-2006-06-13.txt"] = "2006-06-13 21:51:18 -0400" - part_times["response-to-anderson-2006-06-14.txt"] = "2006-07-10 14:31:08 -0400" - part_times["morfin-2006-05-17.pdf"] = "2006-05-17 06:46:18 +0200" - part_times["response-to-morfin-2006-05-17.txt"] = "2006-07-10 14:18:10 -0400" - part_times["anderson-2006-03-08.md"] = "2006-03-08 09:42:44 +0100" - part_times["response-to-anderson-2006-03-08.txt"] = "2006-03-20 14:55:38 -0500" - part_times["morfin-2006-02-20.txt"] = "2006-02-20 19:18:24 +0100" - part_times["response-to-morfin-2006-02-20.txt"] = "2006-03-06 13:08:39 -0500" - part_times["morfin-2006-02-17.txt"] = "2006-02-17 18:59:38 +0100" - part_times["response-to-morfin-2006-02-17.txt"] = "2006-07-10 14:05:15 -0400" - part_times["morfin-2006-02-07.txt"] = "2006-02-07 19:38:57 -0500" - part_times["response-to-morfin-2006-02-07.txt"] = "2006-02-21 19:09:26 -0500" - part_times["morfin-2006-01-14.txt"] = "2006-01-14 15:05:24 +0100" - part_times["response-to-morfin-2006-01-14.txt"] = "2006-02-21 12:23:38 -0500" - part_times["morfin-2005-10-19.txt"] = "2005-10-19 17:12:11 +0200" - part_times["response-to-morfin-2005-10-19.txt"] = "2005-11-15 11:42:30 -0500" - part_times["leibzon-2005-08-29.txt"] = "2005-08-29 08:28:52PDT" - part_times["response-to-leibzon-2005-08-29.txt"] = "2005-12-08 14:04:47 -0500" - part_times["mehnle-2005-08-25.txt"] = "2005-08-25 00:45:26 +0200" - part_times["response-to-mehnle-2005-08-25.txt"] = "2005-12-08 13:37:38 -0500" - part_times["klensin-2005-06-10.txt"] = "2005-06-10 14:49:17 -0400" - part_times["response-to-klensin-2005-06-10.txt"] = "2005-07-22 18:14:06 -0400" - part_times["meyer-2003-11-15.txt"] = "2003-11-15 09:47:11 -0800" - part_times["response-to-meyer-2003-11-15.txt"] = "2003-11-25 10:56:06 -0500" - part_times["glassey-2003-08-06.txt"] = "2003-08-06 02:14:24 +0000" - part_times["response-to-glassey-2003-08-06.txt"] = "2003-09-24 09:54:51 -0400" - part_times["hain-2003-07-31.txt"] = "2003-07-31 16:44:19 -0700" - part_times["response-to-hain-2003-07-31.txt"] = "2003-09-30 14:44:30 -0400" - part_times["zorn-2003-01-15.txt"] = "2003-01-15 01:22:28 -0800" - part_times["elz-2002-11-05.txt"] = "2002-11-05 10:51:13 +0700" - # No time could be found for this one: - part_times["response-to-zorn-2003-01-15.txt"] = "2003-02-08" - # This one was issued sometime between 2002-12-27 (when IESG minutes note that the - # appeal response was approved) and 2003-01-04 (when the appeal was escalated to - # the IAB) - we're using the earlier end of the window - part_times["response-to-elz-2002-11-05.txt"] = "2002-12-27" - for name in part_times: - part_times[name] = dateutil.parser.parse(part_times[name]).astimezone( - datetime.timezone.utc - ) - - redirects = [] - for index, title in enumerate(titles): - # IESG is group 2 - appeal = Appeal.objects.create( - name=titles[index], date=dates[index], group_id=2 - ) - for part in parts[index]: - if part.endswith(".pdf"): - content_type = "application/pdf" - else: - content_type = "text/markdown;charset=utf-8" - if part.endswith(".md"): - source_path = Path(tmpdir) / "iesg_appeals" / part - else: - source_path = Path(old_appeals_root) / part - with source_path.open("rb") as source_file: - bits = source_file.read() - if part == "morfin-2008-09-10.txt": - bits = bits.decode("macintosh") - bits = bits.replace("\r", "\n") - bits = bits.encode("utf8") - elif part in ["morfin-2006-02-07.txt", "morfin-2006-01-14.txt"]: - bits = bits.decode("windows-1252").encode("utf8") - artifact_type_id = ( - "response" if part.startswith("response") else "appeal" - ) - artifact = AppealArtifact.objects.create( - appeal=appeal, - artifact_type_id=artifact_type_id, - date=part_times[part].date(), - content_type=content_type, - bits=bits, - ) - redirects.append( - [ - f'www6.ietf.org/iesg/appeal/{part.replace(".md", ".html") if part.endswith(".md") else part}', - f"https://datatracker.ietf.org/group/iesg/appeals/artifact/{artifact.pk}", - 302, - ] - ) - - shutil.rmtree(tmpdir) - with open("iesg_appeal_redirects.csv", "w", newline="") as f: - csvwriter = csv.writer(f) - for row in redirects: - csvwriter.writerow(row) diff --git a/ietf/group/management/commands/import_iesg_statements.py b/ietf/group/management/commands/import_iesg_statements.py deleted file mode 100644 index 93fdcec161..0000000000 --- a/ietf/group/management/commands/import_iesg_statements.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright The IETF Trust 2024, All Rights Reserved - -import debug # pyflakes:ignore - -import csv -import datetime -import os -import shutil -import subprocess -import tempfile - -from collections import namedtuple, Counter -from pathlib import Path - -from django.conf import settings -from django.core.management.base import BaseCommand - -from ietf.doc.models import Document, DocEvent, State -from ietf.utils.text import xslugify - - -class Command(BaseCommand): - help = "Performs a one-time import of IESG statements" - - def handle(self, *args, **options): - if Document.objects.filter(type="statement", group__acronym="iesg").exists(): - self.stdout.write("IESG statement documents already exist - exiting") - exit(-1) - tmpdir = tempfile.mkdtemp() - process = subprocess.Popen( - ["git", "clone", "https://github.com/kesara/iesg-scraper.git", tmpdir], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - sub_stdout, sub_stderr = process.communicate() - if not Path(tmpdir).joinpath("iesg_statements", "2000-08-29-0.md").exists(): - self.stdout.write( - "Git clone of the iesg-scraper directory did not go as expected" - ) - self.stdout.write("stdout:", sub_stdout) - self.stdout.write("stderr:", sub_stderr) - self.stdout.write(f"Clean up {tmpdir} manually") - exit(-1) - - redirects = [] - for item in self.get_work_items(): - replaced = item.title.endswith( - " SUPERSEDED" - ) or item.doc_time.date() == datetime.date(2007, 7, 30) - title = item.title - if title.endswith(" - SUPERSEDED"): - title = title[: -len(" - SUPERSEDED")] - name = f"statement-iesg-{xslugify(title)}-{item.doc_time:%Y%m%d}" - dest_filename = f"{name}-00.md" - # Create Document - doc = Document.objects.create( - name=name, - type_id="statement", - title=title, - group_id=2, # The IESG group - rev="00", - uploaded_filename=dest_filename, - ) - doc.set_state( - State.objects.get( - type_id="statement", - slug="replaced" if replaced else "active", - ) - ) - e1 = DocEvent.objects.create( - time=item.doc_time, - type="published_statement", - doc=doc, - rev="00", - by_id=1, # (System) - desc="Statement published (note: The exact time of day is inaccurate - the actual time of day is not known)", - ) - e2 = DocEvent.objects.create( - type="added_comment", - doc=doc, - rev="00", - by_id=1, # (System) - desc="Statement moved into datatracker from www.ietf.org", - ) - doc.save_with_history([e1, e2]) - - # Put file in place - source = Path(tmpdir).joinpath("iesg_statements", item.source_filename) - dest = Path(settings.DOCUMENT_PATH_PATTERN.format(doc=doc)).joinpath( - dest_filename - ) - if dest.exists(): - self.stdout.write( - f"WARNING: {dest} already exists - not overwriting it." - ) - else: - os.makedirs(dest.parent, exist_ok=True) - shutil.copy(source, dest) - - redirects.append( - [ - f"www.ietf.org/about/groups/iesg/statements/{item.slug}", - f"https://datatracker.ietf.org/doc/{name}", - 302, - ] - ) - - shutil.rmtree(tmpdir) - with open("iesg_statement_redirects.csv", "w", newline="") as f: - csvwriter = csv.writer(f) - for row in redirects: - csvwriter.writerow(row) - - def get_work_items(self): - Item = namedtuple("Item", "doc_time source_filename title slug") - items = [] - dressed_rows = " ".join( - self.cut_paste_from_www().expandtabs(1).split(" ") - ).split("\n") - old_slugs = self.get_old_slugs() - # Rube-Goldberg-esque dance to deal with conflicting directions of the scrape and - # what order we want the result to sort to - dressed_rows.reverse() - old_slugs.reverse() - total_times_date_seen = Counter([row.split(" ")[0] for row in dressed_rows]) - count_date_seen_so_far = Counter() - for row, slug in zip(dressed_rows, old_slugs): - date_part = row.split(" ")[0] - title_part = row[len(date_part) + 1 :] - datetime_args = list(map(int, date_part.replace("-0", "-").split("-"))) - # Use the minutes in timestamps to preserve order of statements - # on the same day as they currently appear at www.ietf.org - datetime_args.extend([12, count_date_seen_so_far[date_part]]) - count_date_seen_so_far[date_part] += 1 - doc_time = datetime.datetime(*datetime_args, tzinfo=datetime.timezone.utc) - items.append( - Item( - doc_time, - f"{date_part}-{total_times_date_seen[date_part] - count_date_seen_so_far[date_part]}.md", - title_part, - slug, - ) - ) - return items - - def cut_paste_from_www(self): - return """2023-08-24 Support Documents in IETF Working Groups -2023-08-14 Guidance on In-Person and Online Interim Meetings -2023-05-01 IESG Statement on EtherTypes -2023-03-15 Second Report on the RFC 8989 Experiment -2023-01-27 Guidance on In-Person and Online Interim Meetings - SUPERSEDED -2022-10-31 Statement on Restricting Access to IETF IT Systems -2022-01-21 Handling Ballot Positions -2021-09-01 Report on the RFC 8989 experiment -2021-07-21 IESG Statement on Allocation of Email Addresses in the ietf.org Domain -2021-05-11 IESG Statement on Inclusive Language -2021-05-10 IESG Statement on Internet-Draft Authorship -2021-05-07 IESG Processing of RFC Errata for the IETF Stream -2021-04-16 Last Call Guidance to the Community -2020-07-23 IESG Statement On Oppressive or Exclusionary Language -2020-05-01 Guidance on Face-to-Face and Virtual Interim Meetings - SUPERSEDED -2018-03-16 IETF Meeting Photography Policy -2018-01-11 Guidance on Face-to-Face and Virtual Interim Meetings - SUPERSEDED -2017-02-09 License File for Open Source Repositories -2016-11-13 Support Documents in IETF Working Groups - SUPERSEDED -2016-02-05 Guidance on Face-to-Face and Virtual Interim Meetings - SUPERSEDED -2016-01-11 Guidance on Face-to-Face and Virtual Interim Meetings - SUPERSEDED -2015-08-20 IESG Statement on Maximizing Encrypted Access To IETF Information -2015-06-11 IESG Statement on Internet-Draft Authorship - SUPERSEDED -2014-07-20 IESG Statement on Designating RFCs as Historic -2014-05-07 DISCUSS Criteria in IESG Review -2014-03-02 Writable MIB Module IESG Statement -2013-11-03 IETF Anti-Harassment Policy -2012-10-25 IESG Statement on Ethertypes - SUPERSEDED -2012-10-25 IESG Statement on Removal of an Internet-Draft from the IETF Web Site -2011-10-20 IESG Statement on Designating RFCs as Historic - SUPERSEDED -2011-06-27 IESG Statement on Designating RFCs as Historic - SUPERSEDED -2011-06-13 IESG Statement on IESG Processing of RFC Errata concerning RFC Metadata -2010-10-11 IESG Statement on Document Shepherds -2010-05-24 IESG Statement on the Usage of Assignable Codepoints, Addresses and Names in Specification Examples -2010-05-24 IESG Statement on NomCom Eligibility and Day Passes -2009-09-08 IESG Statement on Copyright -2009-01-20 IESG Statement on Proposed Status for IETF Documents Reserving Resources for Example Purposes -2008-09-02 Guidance on Interim Meetings, Conference Calls and Jabber Sessions - SUPERSEDED -2008-07-30 IESG Processing of RFC Errata for the IETF Stream -2008-04-14 IESG Statement on Spam Control on IETF Mailing Lists -2008-03-03 IESG Statement on Registration Requests for URIs Containing Telephone Numbers -2008-02-27 IESG Statement on RFC3406 and URN Namespaces Registry Review -2008-01-23 Advice for WG Chairs Dealing with Off-Topic Postings -2007-10-04 On Appeals of IESG and Area Director Actions and Decisions -2007-07-05 Experimental Specification of New Congestion Control Algorithms -2007-03-20 Guidance on Area Director Sponsoring of Documents -2007-01-15 Last Call Guidance to the Community - SUPERSEDED -2006-04-19 IESG Statement: Normative and Informative References -2006-02-17 IESG Statement on Disruptive Posting -2006-01-09 Guidance for Spam Control on IETF Mailing Lists - SUPERSEDED -2006-01-05 IESG Statement on AUTH48 State -2005-05-12 Syntax for Format Definitions -2003-02-11 IESG Statement on IDN -2002-11-27 Copyright Statement in MIB and PIB Modules -2002-03-13 Guidance for Spam Control on IETF Mailing Lists - SUPERSEDED -2001-12-21 On Design Teams -2001-10-01 Guidelines for the Use of Formal Languages in IETF Specifications -2001-03-21 Establishment of Temporary Sub-IP Area -2000-12-06 Plans to Organize "Sub-IP" Technologies in the IETF -2000-11-20 A New IETF Work Area -2000-08-29 Guidance on Interim IETF Working Group Meetings and Conference Calls - SUPERSEDED -2000-08-29 IESG Guidance on the Moderation of IETF Working Group Mailing Lists""" - - def get_old_slugs(self): - return [ - "support-documents", - "interim-meetings-guidance", - "ethertypes", - "second-report-on-the-rfc-8989-experiment", - "interim-meetings-guidance-2023-01-27", - "statement-on-restricting-access", - "handling-ballot-positions", - "report-on-rfc8989-experiment", - "email-addresses-ietf-domain", - "on-inclusive-language", - "internet-draft-authorship", - "processing-errata-ietf-stream", - "last-call-guidance", - "statement-on-oppressive-exclusionary-language", - "interim-meetings-guidance-2020-05-01", - "meeting-photography-policy", - "interim-meetings-guidance-2018-01-11", - "open-source-repositories-license", - "support-documents-2016-11-13", - "interim-meetings-guidance-2016-02-05", - "interim-meetings-guidance-2016-01-11", - "maximizing-encrypted-access", - "internet-draft-authorship-2015-06-11", - "designating-rfcs-historic", - "iesg-discuss-criteria", - "writable-mib-module", - "anti-harassment-policy", - "ethertypes-2012-10-25", - "internet-draft-removal", - "designating-rfcs-historic-2011-10-20", - "designating-rfcs-historic-2011-06-27", - "rfc-metadata-errata", - "document-shepherds", - "assignable-codepoints-addresses-names", - "nomcom-eligibility-day-passes", - "copyright-2009-09-08", - "reserving-resources-examples", - "interim-meetings-guidance-2008-09-02", - "processing-rfc-errata", - "spam-control-2008-04-14", - "registration-requests-uris", - "urn-namespaces-registry", - "off-topic-postings", - "appeals-actions-decisions", - "experimental-congestion-control", - "area-director-sponsoring-documents", - "last-call-guidance-2007-01-15", - "normative-informative-references", - "disruptive-posting", - "spam-control-2006-01-09", - "auth48", - "syntax-format-definitions", - "idn", - "copyright-2002-11-27", - "spam-control-2002-03-13", - "design-teams", - "formal-languages-use", - "sub-ip-area-2001-03-21", - "sub-ip-area-2000-11-20", - "sub-ip-area-2000-12-06", - "interim-meetings-guidance-2000-08-29", - "mailing-lists-moderation", - ] diff --git a/ietf/group/tasks.py b/ietf/group/tasks.py new file mode 100644 index 0000000000..f19246fb55 --- /dev/null +++ b/ietf/group/tasks.py @@ -0,0 +1,99 @@ +# Copyright The IETF Trust 2024, All Rights Reserved +# +# Celery task definitions +# +import shutil + +from celery import shared_task +from pathlib import Path + +from django.conf import settings +from django.template.loader import render_to_string + +from ietf.utils import log + +from .models import Group +from .utils import fill_in_charter_info, fill_in_wg_drafts, fill_in_wg_roles +from .views import extract_last_name, roles + + +@shared_task +def generate_wg_charters_files_task(): + areas = Group.objects.filter(type="area", state="active").order_by("name") + groups = ( + Group.objects.filter(type="wg", state="active") + .exclude(parent=None) + .order_by("acronym") + ) + for group in groups: + fill_in_charter_info(group) + fill_in_wg_roles(group) + fill_in_wg_drafts(group) + for area in areas: + area.groups = [g for g in groups if g.parent_id == area.pk] + charter_path = Path(settings.CHARTER_PATH) + charters_file = charter_path / "1wg-charters.txt" + charters_file.write_text( + render_to_string("group/1wg-charters.txt", {"areas": areas}), + encoding="utf8", + ) + charters_by_acronym_file = charter_path / "1wg-charters-by-acronym.txt" + charters_by_acronym_file.write_text( + render_to_string("group/1wg-charters-by-acronym.txt", {"groups": groups}), + encoding="utf8", + ) + + charter_copy_dest = getattr(settings, "CHARTER_COPY_PATH", None) + if charter_copy_dest is not None: + if not Path(charter_copy_dest).is_dir(): + log.log( + f"Error copying 1wg-charter files to {charter_copy_dest}: it does not exist or is not a directory" + ) + else: + try: + shutil.copy2(charters_file, charter_copy_dest) + except IOError as err: + log.log(f"Error copying {charters_file} to {charter_copy_dest}: {err}") + try: + shutil.copy2(charters_by_acronym_file, charter_copy_dest) + except IOError as err: + log.log( + f"Error copying {charters_by_acronym_file} to {charter_copy_dest}: {err}" + ) + + +@shared_task +def generate_wg_summary_files_task(): + # Active WGs (all should have a parent, but filter to be sure) + groups = ( + Group.objects.filter(type="wg", state="active") + .exclude(parent=None) + .order_by("acronym") + ) + # Augment groups with chairs list + for group in groups: + group.chairs = sorted(roles(group, "chair"), key=extract_last_name) + + # Active areas with one or more active groups in them + areas = Group.objects.filter( + type="area", + state="active", + group__in=groups, + ).distinct().order_by("name") + # Augment areas with their groups + for area in areas: + area.groups = [g for g in groups if g.parent_id == area.pk] + summary_path = Path(settings.GROUP_SUMMARY_PATH) + summary_file = summary_path / "1wg-summary.txt" + summary_file.write_text( + render_to_string("group/1wg-summary.txt", {"areas": areas}), + encoding="utf8", + ) + summary_by_acronym_file = summary_path / "1wg-summary-by-acronym.txt" + summary_by_acronym_file.write_text( + render_to_string( + "group/1wg-summary-by-acronym.txt", + {"areas": areas, "groups": groups}, + ), + encoding="utf8", + ) diff --git a/ietf/group/tests.py b/ietf/group/tests.py index 66a854000d..130c68b3fc 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -1,15 +1,10 @@ # Copyright The IETF Trust 2013-2020, All Rights Reserved # -*- coding: utf-8 -*- -import io -import os import datetime import json +import mock -from tempfile import NamedTemporaryFile - -from django.core.management import call_command -from django.conf import settings from django.urls import reverse as urlreverse from django.db.models import Q from django.test import Client @@ -20,10 +15,17 @@ from ietf.doc.factories import DocumentFactory, WgDraftFactory, EditorialDraftFactory from ietf.doc.models import DocEvent, RelatedDocument, Document from ietf.group.models import Role, Group -from ietf.group.utils import get_group_role_emails, get_child_group_role_emails, get_group_ad_emails, GroupAliasGenerator +from ietf.group.utils import ( + get_group_role_emails, + get_child_group_role_emails, + get_group_ad_emails, + get_group_email_aliases, + GroupAliasGenerator, + role_holder_emails, +) from ietf.group.factories import GroupFactory, RoleFactory from ietf.person.factories import PersonFactory, EmailFactory -from ietf.person.models import Person +from ietf.person.models import Email, Person from ietf.utils.test_utils import login_testing_unauthorized, TestCase class StreamTests(TestCase): @@ -128,126 +130,6 @@ def test_group_document_dependencies(self): class GenerateGroupAliasesTests(TestCase): - def setUp(self): - super().setUp() - self.doc_aliases_file = NamedTemporaryFile(delete=False, mode='w+') - self.doc_aliases_file.close() - self.doc_virtual_file = NamedTemporaryFile(delete=False, mode='w+') - self.doc_virtual_file.close() - self.saved_draft_aliases_path = settings.GROUP_ALIASES_PATH - self.saved_draft_virtual_path = settings.GROUP_VIRTUAL_PATH - settings.GROUP_ALIASES_PATH = self.doc_aliases_file.name - settings.GROUP_VIRTUAL_PATH = self.doc_virtual_file.name - - def tearDown(self): - settings.GROUP_ALIASES_PATH = self.saved_draft_aliases_path - settings.GROUP_VIRTUAL_PATH = self.saved_draft_virtual_path - os.unlink(self.doc_aliases_file.name) - os.unlink(self.doc_virtual_file.name) - super().tearDown() - - def testManagementCommand(self): - a_month_ago = timezone.now() - datetime.timedelta(30) - a_decade_ago = timezone.now() - datetime.timedelta(3650) - role1 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='myth', group__state_id='active') - area = role1.group - ad = role1.person - mars = GroupFactory(type_id='wg', acronym='mars', parent=area) - marschair = PersonFactory(user__username='marschair') - mars.role_set.create(name_id='chair', person=marschair, email=marschair.email()) - marssecr = PersonFactory(user__username='marssecr') - mars.role_set.create(name_id='secr', person=marssecr, email=marssecr.email()) - ames = GroupFactory(type_id='wg', acronym='ames', parent=area) - ameschair = PersonFactory(user__username='ameschair') - ames.role_set.create(name_id='chair', person=ameschair, email=ameschair.email()) - recent = GroupFactory(type_id='wg', acronym='recent', parent=area, state_id='conclude', time=a_month_ago) - recentchair = PersonFactory(user__username='recentchair') - recent.role_set.create(name_id='chair', person=recentchair, email=recentchair.email()) - wayold = GroupFactory(type_id='wg', acronym='wayold', parent=area, state_id='conclude', time=a_decade_ago) - wayoldchair = PersonFactory(user__username='wayoldchair') - wayold.role_set.create(name_id='chair', person=wayoldchair, email=wayoldchair.email()) - role2 = RoleFactory(name_id='ad', group__type_id='area', group__acronym='done', group__state_id='conclude') - done = role2.group - done_ad = role2.person - irtf = Group.objects.get(acronym='irtf') - testrg = GroupFactory(type_id='rg', acronym='testrg', parent=irtf) - testrgchair = PersonFactory(user__username='testrgchair') - testrg.role_set.create(name_id='chair', person=testrgchair, email=testrgchair.email()) - testrag = GroupFactory(type_id='rg', acronym='testrag', parent=irtf) - testragchair = PersonFactory(user__username='testragchair') - testrag.role_set.create(name_id='chair', person=testragchair, email=testragchair.email()) - individual = PersonFactory() - - args = [ ] - kwargs = { } - out = io.StringIO() - call_command("generate_group_aliases", *args, **kwargs, stdout=out, stderr=out) - self.assertFalse(out.getvalue()) - - with open(settings.GROUP_ALIASES_PATH) as afile: - acontent = afile.read() - self.assertTrue('xfilter-' + area.acronym + '-ads' in acontent) - self.assertTrue('xfilter-' + area.acronym + '-chairs' in acontent) - self.assertTrue('xfilter-' + mars.acronym + '-ads' in acontent) - self.assertTrue('xfilter-' + mars.acronym + '-chairs' in acontent) - self.assertTrue('xfilter-' + ames.acronym + '-ads' in acontent) - self.assertTrue('xfilter-' + ames.acronym + '-chairs' in acontent) - self.assertTrue(all([x in acontent for x in [ - 'xfilter-' + area.acronym + '-ads', - 'xfilter-' + area.acronym + '-chairs', - 'xfilter-' + mars.acronym + '-ads', - 'xfilter-' + mars.acronym + '-chairs', - 'xfilter-' + ames.acronym + '-ads', - 'xfilter-' + ames.acronym + '-chairs', - 'xfilter-' + recent.acronym + '-ads', - 'xfilter-' + recent.acronym + '-chairs', - ]])) - self.assertFalse(all([x in acontent for x in [ - 'xfilter-' + done.acronym + '-ads', - 'xfilter-' + done.acronym + '-chairs', - 'xfilter-' + wayold.acronym + '-ads', - 'xfilter-' + wayold.acronym + '-chairs', - ]])) - - with open(settings.GROUP_VIRTUAL_PATH) as vfile: - vcontent = vfile.read() - self.assertTrue(all([x in vcontent for x in [ - ad.email_address(), - marschair.email_address(), - marssecr.email_address(), - ameschair.email_address(), - recentchair.email_address(), - testrgchair.email_address(), - testragchair.email_address(), - ]])) - self.assertFalse(any([x in vcontent for x in [ - done_ad.email_address(), - wayoldchair.email_address(), - individual.email_address(), - ]])) - self.assertTrue(all([x in vcontent for x in [ - 'xfilter-' + area.acronym + '-ads', - 'xfilter-' + area.acronym + '-chairs', - 'xfilter-' + mars.acronym + '-ads', - 'xfilter-' + mars.acronym + '-chairs', - 'xfilter-' + ames.acronym + '-ads', - 'xfilter-' + ames.acronym + '-chairs', - 'xfilter-' + recent.acronym + '-ads', - 'xfilter-' + recent.acronym + '-chairs', - 'xfilter-' + testrg.acronym + '-chairs', - 'xfilter-' + testrag.acronym + '-chairs', - testrg.acronym + '-chairs@ietf.org', - testrg.acronym + '-chairs@irtf.org', - testrag.acronym + '-chairs@ietf.org', - testrag.acronym + '-chairs@irtf.org', - ]])) - self.assertFalse(all([x in vcontent for x in [ - 'xfilter-' + done.acronym + '-ads', - 'xfilter-' + done.acronym + '-chairs', - 'xfilter-' + wayold.acronym + '-ads', - 'xfilter-' + wayold.acronym + '-chairs', - ]])) - def test_generator_class(self): """The GroupAliasGenerator should generate the same lists as the old mgmt cmd""" # clean out test fixture group roles we don't need for this test @@ -306,6 +188,66 @@ def test_generator_class(self): {k: (sorted(doms), sorted(addrs)) for k, (doms, addrs) in expected_dict.items()}, ) + @mock.patch("ietf.group.utils.GroupAliasGenerator") + def test_get_group_email_aliases(self, mock_alias_gen_cls): + GroupFactory(name="agroup", type_id="rg") + GroupFactory(name="bgroup") + GroupFactory(name="cgroup", type_id="rg") + GroupFactory(name="dgroup") + + mock_alias_gen_cls.return_value = [ + ("bgroup-chairs", ["ietf"], ["c1@example.com", "c2@example.com"]), + ("agroup-ads", ["ietf", "irtf"], ["ad@example.com"]), + ("bgroup-ads", ["ietf"], ["ad@example.com"]), + ] + # order is important - should be by acronym, otherwise left in order returned by generator + self.assertEqual( + get_group_email_aliases(None, None), + [ + { + "acronym": "agroup", + "alias_type": "-ads", + "expansion": "ad@example.com", + }, + { + "acronym": "bgroup", + "alias_type": "-chairs", + "expansion": "c1@example.com, c2@example.com", + }, + { + "acronym": "bgroup", + "alias_type": "-ads", + "expansion": "ad@example.com", + }, + ], + ) + self.assertQuerySetEqual( + mock_alias_gen_cls.call_args[0][0], + Group.objects.all(), + ordered=False, + ) + + # test other parameter combinations but we already checked that the alias generator's + # output will be passed through, so don't re-test the processing + get_group_email_aliases("agroup", None) + self.assertQuerySetEqual( + mock_alias_gen_cls.call_args[0][0], + Group.objects.filter(acronym="agroup"), + ordered=False, + ) + get_group_email_aliases(None, "wg") + self.assertQuerySetEqual( + mock_alias_gen_cls.call_args[0][0], + Group.objects.filter(type_id="wg"), + ordered=False, + ) + get_group_email_aliases("agroup", "wg") + self.assertQuerySetEqual( + mock_alias_gen_cls.call_args[0][0], + Group.objects.none(), + ordered=False, + ) + class GroupRoleEmailTests(TestCase): @@ -344,3 +286,41 @@ def test_group_ad_emails(self): self.assertGreater(len(emails), 0) for item in emails: self.assertIn('@', item) + + def test_role_holder_emails(self): + # The test fixtures create a bunch of addresses that pollute this test's results - disable them + Email.objects.update(active=False) + + role_holders = [ + RoleFactory(name_id="member", group__type_id=gt).person + for gt in [ + "ag", + "area", + "dir", + "iab", + "ietf", + "irtf", + "nomcom", + "rg", + "team", + "wg", + "rag", + ] + ] + # Expect an additional active email to be included + EmailFactory( + person=role_holders[0], + active=True, + ) + # Do not expect an inactive email to be included + EmailFactory( + person=role_holders[1], + active=False, + ) + # Do not expect address on a role-holder for a different group type + RoleFactory(name_id="member", group__type_id="adhoc") # arbitrary type not in the of-interest list + + self.assertCountEqual( + role_holder_emails(), + Email.objects.filter(active=True, person__in=role_holders), + ) diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py index 6ecac7d347..561f542f42 100644 --- a/ietf/group/tests_info.py +++ b/ietf/group/tests_info.py @@ -2,21 +2,22 @@ # -*- coding: utf-8 -*- -import os import calendar import datetime import io import bleach +import mock -from unittest.mock import patch +from unittest.mock import call, patch from pathlib import Path from pyquery import PyQuery -from tempfile import NamedTemporaryFile import debug # pyflakes:ignore from django.conf import settings +from django.http import Http404, HttpResponse from django.test import RequestFactory +from django.test.utils import override_settings from django.urls import reverse as urlreverse from django.urls import NoReverseMatch from django.utils import timezone @@ -34,6 +35,8 @@ DatedGroupMilestoneFactory, DatelessGroupMilestoneFactory) from ietf.group.forms import GroupForm from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, Role +from ietf.group.tasks import generate_wg_charters_files_task, generate_wg_summary_files_task +from ietf.group.views import response_from_file from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group from ietf.meeting.factories import SessionFactory from ietf.name.models import DocTagName, GroupStateName, GroupTypeName, ExtResourceName, RoleName @@ -56,7 +59,11 @@ def pklist(docs): return [ str(doc.pk) for doc in docs.all() ] class GroupPagesTests(TestCase): - settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['CHARTER_PATH'] + settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [ + "CHARTER_PATH", + "CHARTER_COPY_PATH", + "GROUP_SUMMARY_PATH", + ] def test_active_groups(self): area = GroupFactory.create(type_id='area') @@ -110,49 +117,198 @@ def test_group_home(self): self.assertContains(r, draft.name) self.assertContains(r, draft.title) - def test_wg_summaries(self): - group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area')).group - RoleFactory(group=group,name_id='chair',person=PersonFactory()) - RoleFactory(group=group,name_id='ad',person=PersonFactory()) + def test_response_from_file(self): + # n.b., GROUP_SUMMARY_PATH is a temp dir that will be cleaned up automatically + fp = Path(settings.GROUP_SUMMARY_PATH) / "some-file.txt" + fp.write_text("This is a charters file with an é") + r = response_from_file(fp) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.headers["Content-Type"], "text/plain; charset=utf-8") + self.assertEqual(r.content.decode("utf8"), "This is a charters file with an é") + # now try with a nonexistent file + fp.unlink() + with self.assertRaises(Http404): + response_from_file(fp) + + @patch("ietf.group.views.response_from_file") + def test_wg_summary_area(self, mock): + r = self.client.get( + urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "rg"}) + ) # not wg + self.assertEqual(r.status_code, 404) + self.assertFalse(mock.called) + mock.return_value = HttpResponse("yay") + r = self.client.get( + urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "wg"}) + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content.decode(), "yay") + self.assertEqual(mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt")) + + @patch("ietf.group.views.response_from_file") + def test_wg_summary_acronym(self, mock): + r = self.client.get( + urlreverse( + "ietf.group.views.wg_summary_acronym", kwargs={"group_type": "rg"} + ) + ) # not wg + self.assertEqual(r.status_code, 404) + self.assertFalse(mock.called) + mock.return_value = HttpResponse("yay") + r = self.client.get( + urlreverse( + "ietf.group.views.wg_summary_acronym", kwargs={"group_type": "wg"} + ) + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content.decode(), "yay") + self.assertEqual( + mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt") + ) + @patch("ietf.group.views.response_from_file") + def test_wg_charters(self, mock): + r = self.client.get( + urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "rg"}) + ) # not wg + self.assertEqual(r.status_code, 404) + self.assertFalse(mock.called) + mock.return_value = HttpResponse("yay") + r = self.client.get( + urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "wg"}) + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content.decode(), "yay") + self.assertEqual(mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters.txt")) + + @patch("ietf.group.views.response_from_file") + def test_wg_charters_by_acronym(self, mock): + r = self.client.get( + urlreverse( + "ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "rg"} + ) + ) # not wg + self.assertEqual(r.status_code, 404) + self.assertFalse(mock.called) + mock.return_value = HttpResponse("yay") + r = self.client.get( + urlreverse( + "ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "wg"} + ) + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.content.decode(), "yay") + self.assertEqual( + mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt") + ) + + def test_generate_wg_charters_files_task(self): + group = CharterFactory( + group__type_id="wg", group__parent=GroupFactory(type_id="area") + ).group + RoleFactory(group=group, name_id="chair", person=PersonFactory()) + RoleFactory(group=group, name_id="ad", person=PersonFactory()) chair = Email.objects.filter(role__group=group, role__name="chair")[0] + ( + Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt" + ).write_text("This is a charter.") + generate_wg_charters_files_task() + wg_charters_contents = (Path(settings.CHARTER_PATH) / "1wg-charters.txt").read_text( + encoding="utf8" + ) + self.assertIn(group.acronym, wg_charters_contents) + self.assertIn(group.name, wg_charters_contents) + self.assertIn(group.ad_role().person.plain_name(), wg_charters_contents) + self.assertIn(chair.address, wg_charters_contents) + self.assertIn("This is a charter.", wg_charters_contents) + wg_charters_copy = ( + Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt" + ).read_text(encoding="utf8") + self.assertEqual(wg_charters_copy, wg_charters_contents) + + wg_charters_by_acronym_contents = ( + Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt" + ).read_text(encoding="utf8") + self.assertIn(group.acronym, wg_charters_by_acronym_contents) + self.assertIn(group.name, wg_charters_by_acronym_contents) + self.assertIn(group.ad_role().person.plain_name(), wg_charters_by_acronym_contents) + self.assertIn(chair.address, wg_charters_by_acronym_contents) + self.assertIn("This is a charter.", wg_charters_by_acronym_contents) + wg_charters_by_acronymcopy = ( + Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt" + ).read_text(encoding="utf8") + self.assertEqual(wg_charters_by_acronymcopy, wg_charters_by_acronym_contents) + + def test_generate_wg_charters_files_task_without_copy(self): + """Test disabling charter file copying + + Note that these tests mostly check that errors are not encountered. Because they unset + the CHARTER_COPY_PATH or set it to a non-directory destination, it's not clear where to + look to see whether the files were (incorrectly) copied somewhere. + """ + group = CharterFactory( + group__type_id="wg", group__parent=GroupFactory(type_id="area") + ).group ( Path(settings.CHARTER_PATH) / f"{group.charter.name}-{group.charter.rev}.txt" - ).write_text("This is a charter.") + ).write_text("This is a charter.") - url = urlreverse('ietf.group.views.wg_summary_area', kwargs=dict(group_type="wg")) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertContains(r, group.parent.name) - self.assertContains(r, group.acronym) - self.assertContains(r, group.name) - self.assertContains(r, chair.address) + # No directory set + with override_settings(): + del settings.CHARTER_COPY_PATH + generate_wg_charters_files_task() + # n.b., CHARTER_COPY_PATH is set again outside the with block + self.assertTrue((Path(settings.CHARTER_PATH) / "1wg-charters.txt").exists()) + self.assertFalse((Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt").exists()) + self.assertTrue( + (Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").exists() + ) + self.assertFalse( + (Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt").exists() + ) + (Path(settings.CHARTER_PATH) / "1wg-charters.txt").unlink() + (Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").unlink() + + # Set to a file, not a directory + not_a_dir = Path(settings.CHARTER_COPY_PATH) / "not-a-dir.txt" + not_a_dir.write_text("Not a dir") + with override_settings(CHARTER_COPY_PATH=str(not_a_dir)): + generate_wg_charters_files_task() + # n.b., CHARTER_COPY_PATH is set again outside the with block + self.assertTrue((Path(settings.CHARTER_PATH) / "1wg-charters.txt").exists()) + self.assertFalse((Path(settings.CHARTER_COPY_PATH) / "1wg-charters.txt").exists()) + self.assertTrue( + (Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt").exists() + ) + self.assertFalse( + (Path(settings.CHARTER_COPY_PATH) / "1wg-charters-by-acronym.txt").exists() + ) + self.assertEqual(not_a_dir.read_text(), "Not a dir") - url = urlreverse('ietf.group.views.wg_summary_acronym', kwargs=dict(group_type="wg")) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertContains(r, group.acronym) - self.assertContains(r, group.name) - self.assertContains(r, chair.address) - - url = urlreverse('ietf.group.views.wg_charters', kwargs=dict(group_type="wg")) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertContains(r, group.acronym) - self.assertContains(r, group.name) - self.assertContains(r, group.ad_role().person.plain_name()) - self.assertContains(r, chair.address) - self.assertContains(r, "This is a charter.") + def test_generate_wg_summary_files_task(self): + group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area')).group + RoleFactory(group=group,name_id='chair',person=PersonFactory()) + RoleFactory(group=group,name_id='ad',person=PersonFactory()) - url = urlreverse('ietf.group.views.wg_charters_by_acronym', kwargs=dict(group_type="wg")) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - self.assertContains(r, group.acronym) - self.assertContains(r, group.name) - self.assertContains(r, group.ad_role().person.plain_name()) - self.assertContains(r, chair.address) - self.assertContains(r, "This is a charter.") + chair = Email.objects.filter(role__group=group, role__name="chair")[0] + + generate_wg_summary_files_task() + + summary_by_area_contents = ( + Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt" + ).read_text(encoding="utf8") + self.assertIn(group.parent.name, summary_by_area_contents) + self.assertIn(group.acronym, summary_by_area_contents) + self.assertIn(group.name, summary_by_area_contents) + self.assertIn(chair.address, summary_by_area_contents) + + summary_by_acronym_contents = ( + Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt" + ).read_text(encoding="utf8") + self.assertIn(group.acronym, summary_by_acronym_contents) + self.assertIn(group.name, summary_by_acronym_contents) + self.assertIn(chair.address, summary_by_acronym_contents) def test_chartering_groups(self): group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area'),states=[('charter','intrev')]).group @@ -1768,58 +1924,72 @@ def setUp(self): PersonFactory(user__username='plain') GroupFactory(acronym='mars',parent=GroupFactory(type_id='area')) GroupFactory(acronym='ames',parent=GroupFactory(type_id='area')) - self.group_alias_file = NamedTemporaryFile(delete=False) - self.group_alias_file.write(b"""# Generated by hand at 2015-02-12_16:30:52 -virtual.ietf.org anything -mars-ads@ietf.org xfilter-mars-ads -expand-mars-ads@virtual.ietf.org aread@example.org -mars-chairs@ietf.org xfilter-mars-chairs -expand-mars-chairs@virtual.ietf.org mars_chair@ietf.org -ames-ads@ietf.org xfilter-mars-ads -expand-ames-ads@virtual.ietf.org aread@example.org -ames-chairs@ietf.org xfilter-mars-chairs -expand-ames-chairs@virtual.ietf.org mars_chair@ietf.org -""") - self.group_alias_file.close() - self.saved_group_virtual_path = settings.GROUP_VIRTUAL_PATH - settings.GROUP_VIRTUAL_PATH = self.group_alias_file.name - - def tearDown(self): - settings.GROUP_VIRTUAL_PATH = self.saved_group_virtual_path - os.unlink(self.group_alias_file.name) - super().tearDown() - - def testAliases(self): + + @mock.patch("ietf.group.views.get_group_email_aliases") + def testAliases(self, mock_get_aliases): url = urlreverse('ietf.group.urls_info_details.redirect.email', kwargs=dict(acronym="mars")) r = self.client.get(url) self.assertEqual(r.status_code, 302) + mock_get_aliases.return_value = [ + {"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + ] for testdict in [dict(acronym="mars"),dict(acronym="mars",group_type="wg")]: url = urlreverse('ietf.group.urls_info_details.redirect.email', kwargs=testdict) r = self.client.get(url,follow=True) + self.assertEqual( + mock_get_aliases.call_args, + mock.call(testdict.get("acronym", None), testdict.get("group_type", None)), + ) self.assertTrue(all([x in unicontent(r) for x in ['mars-ads@','mars-chairs@']])) self.assertFalse(any([x in unicontent(r) for x in ['ames-ads@','ames-chairs@']])) url = urlreverse('ietf.group.views.email_aliases', kwargs=dict()) login_testing_unauthorized(self, "plain", url) + + mock_get_aliases.return_value = [ + {"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + {"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + ] r = self.client.get(url) self.assertTrue(r.status_code,200) + self.assertEqual(mock_get_aliases.call_args, mock.call(None, None)) self.assertTrue(all([x in unicontent(r) for x in ['mars-ads@','mars-chairs@','ames-ads@','ames-chairs@']])) url = urlreverse('ietf.group.views.email_aliases', kwargs=dict(group_type="wg")) + mock_get_aliases.return_value = [ + {"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + {"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + ] r = self.client.get(url) self.assertEqual(r.status_code,200) + self.assertEqual(mock_get_aliases.call_args, mock.call(None, "wg")) self.assertContains(r, 'mars-ads@') url = urlreverse('ietf.group.views.email_aliases', kwargs=dict(group_type="rg")) + mock_get_aliases.return_value = [] r = self.client.get(url) self.assertEqual(r.status_code,200) + self.assertEqual(mock_get_aliases.call_args, mock.call(None, "rg")) self.assertNotContains(r, 'mars-ads@') - def testExpansions(self): + @mock.patch("ietf.group.views.get_group_email_aliases") + def testExpansions(self, mock_get_aliases): + mock_get_aliases.return_value = [ + {"acronym": "mars", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "mars", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + {"acronym": "ames", "alias_type": "-ads", "expansion": "aread@example.org"}, + {"acronym": "ames", "alias_type": "-chairs", "expansion": "mars_chair@ietf.org"}, + ] url = urlreverse('ietf.group.views.email', kwargs=dict(acronym="mars")) r = self.client.get(url) self.assertEqual(r.status_code,200) + self.assertEqual(mock_get_aliases.call_args, mock.call("mars", None)) self.assertContains(r, 'Email aliases') self.assertContains(r, 'mars-ads@ietf.org') self.assertContains(r, 'group_personnel_change') diff --git a/ietf/group/utils.py b/ietf/group/utils.py index 36917d3124..68b618120b 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -15,13 +15,13 @@ from ietf.community.models import CommunityList, SearchRule from ietf.community.utils import reset_name_contains_index_for_rule, can_manage_community_list -from ietf.doc.models import Document, State +from ietf.doc.models import Document, State, RelatedDocument from ietf.group.models import Group, RoleHistory, Role, GroupFeatures, GroupEvent from ietf.ietfauth.utils import has_role from ietf.name.models import GroupTypeName, RoleName from ietf.person.models import Email from ietf.review.utils import can_manage_review_requests_for_team -from ietf.utils import log +from ietf.utils import log, markdown from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history from ietf.doc.templatetags.ietf_filters import is_valid_url from functools import reduce @@ -373,6 +373,12 @@ class GroupAliasGenerator: ] # This should become groupfeature driven... no_ad_group_types = ["rg", "rag", "team", "program", "rfcedtyp", "edappr", "edwg"] + def __init__(self, group_queryset=None): + if group_queryset is None: + self.group_queryset = Group.objects.all() + else: + self.group_queryset = group_queryset + def __iter__(self): show_since = timezone.now() - datetime.timedelta(days=self.days) @@ -384,7 +390,7 @@ def __iter__(self): if g == "program": domains.append("iab") - entries = Group.objects.filter(type=g).all() + entries = self.group_queryset.filter(type=g).all() active_entries = entries.filter(state__in=self.active_states) inactive_recent_entries = entries.exclude( state__in=self.active_states @@ -405,7 +411,7 @@ def __iter__(self): yield name + "-chairs", domains, list(chair_emails) # The area lists include every chair in active working groups in the area - areas = Group.objects.filter(type="area").all() + areas = self.group_queryset.filter(type="area").all() active_areas = areas.filter(state__in=self.active_states) for area in active_areas: name = area.acronym @@ -418,10 +424,118 @@ def __iter__(self): # Other groups with chairs that require Internet-Draft submission approval gtypes = GroupTypeName.objects.values_list("slug", flat=True) - special_groups = Group.objects.filter( + special_groups = self.group_queryset.filter( type__features__req_subm_approval=True, acronym__in=gtypes, state="active" ) for group in special_groups: chair_emails = get_group_role_emails(group, ["chair", "delegate"]) if chair_emails: yield group.acronym + "-chairs", ["ietf"], list(chair_emails) + + +def get_group_email_aliases(acronym, group_type): + aliases = [] + group_queryset = Group.objects.all() + if acronym: + group_queryset = group_queryset.filter(acronym=acronym) + if group_type: + group_queryset = group_queryset.filter(type__slug=group_type) + for (alias, _, alist) in GroupAliasGenerator(group_queryset): + acro, _hyphen, alias_type = alias.partition("-") + expansion = ", ".join(sorted(alist)) + aliases.append({ + "acronym": acro, + "alias_type": f"-{alias_type}" if alias_type else "", + "expansion": expansion, + }) + return sorted(aliases, key=lambda a: a["acronym"]) + + +def role_holder_emails(): + """Get queryset of active Emails for group role holders""" + group_types_of_interest = [ + "ag", + "area", + "dir", + "iab", + "ietf", + "irtf", + "nomcom", + "rg", + "team", + "wg", + "rag", + ] + roles = Role.objects.filter( + group__state__slug="active", + group__type__in=group_types_of_interest, + ) + emails = Email.objects.filter(active=True).exclude( + address__startswith="unknown-email-" + ) + return emails.filter(person__role__in=roles).distinct() + + +def fill_in_charter_info(group, include_drafts=False): + group.areadirector = getattr(group.ad_role(),'email',None) + + personnel = {} + for r in Role.objects.filter(group=group).order_by('person__name').select_related("email", "person", "name"): + if r.name_id not in personnel: + personnel[r.name_id] = [] + personnel[r.name_id].append(r) + + if group.parent and group.parent.type_id == "area" and group.ad_role() and "ad" not in personnel: + ad_roles = list(Role.objects.filter(group=group.parent, name="ad", person=group.ad_role().person)) + if ad_roles: + personnel["ad"] = ad_roles + + group.personnel = [] + for role_name_slug, roles in personnel.items(): + label = roles[0].name.name + if len(roles) > 1: + if label.endswith("y"): + label = label[:-1] + "ies" + else: + label += "s" + + group.personnel.append((role_name_slug, label, roles)) + + group.personnel.sort(key=lambda t: t[2][0].name.order) + + milestone_state = "charter" if group.state_id == "proposed" else "active" + group.milestones = group.groupmilestone_set.filter(state=milestone_state) + if group.uses_milestone_dates: + group.milestones = group.milestones.order_by('resolved', 'due') + else: + group.milestones = group.milestones.order_by('resolved', 'order') + + if group.charter: + group.charter_text = get_charter_text(group) + else: + group.charter_text = "Not chartered yet." + group.charter_html = markdown.markdown(group.charter_text) + + +def fill_in_wg_roles(group): + def get_roles(slug, default): + for role_slug, label, roles in group.personnel: + if slug == role_slug: + return roles + return default + + group.chairs = get_roles("chair", []) + ads = get_roles("ad", []) + group.areadirector = ads[0] if ads else None + group.techadvisors = get_roles("techadv", []) + group.editors = get_roles("editor", []) + group.secretaries = get_roles("secr", []) + + +def fill_in_wg_drafts(group): + group.drafts = Document.objects.filter(type_id="draft", group=group).order_by("name") + group.rfcs = Document.objects.filter(type_id="rfc", group=group).order_by("rfc_number") + for rfc in group.rfcs: + # TODO: remote_field? + rfc.remote_field = RelatedDocument.objects.filter(source=rfc,relationship_id__in=['obs','updates']).distinct() + rfc.invrel = RelatedDocument.objects.filter(target=rfc,relationship_id__in=['obs','updates']).distinct() diff --git a/ietf/group/views.py b/ietf/group/views.py index 636871d901..f909a31b6d 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -37,13 +37,12 @@ import copy import datetime import itertools -import io import math -import re import json +import types from collections import OrderedDict, defaultdict -import types +from pathlib import Path from simple_history.utils import update_change_reason from django import forms @@ -75,12 +74,13 @@ from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, ChangeStateGroupEvent, GroupFeatures, AppealArtifact ) -from ietf.group.utils import (get_charter_text, can_manage_all_groups_of_type, +from ietf.group.utils import (can_manage_all_groups_of_type, milestone_reviewer_for_group_type, can_provide_status_update, can_manage_materials, group_attribute_change_desc, construct_group_menu_context, get_group_materials, save_group_in_history, can_manage_group, update_role_set, - get_group_or_404, setup_default_community_list_for_group, ) + get_group_or_404, setup_default_community_list_for_group, fill_in_charter_info, + get_group_email_aliases) # from ietf.ietfauth.utils import has_role, is_authorized_in_group from ietf.mailtrigger.utils import gather_relevant_expansions @@ -132,84 +132,17 @@ def roles(group, role_name): return Role.objects.filter(group=group, name=role_name).select_related("email", "person") -def fill_in_charter_info(group, include_drafts=False): - group.areadirector = getattr(group.ad_role(),'email',None) - - personnel = {} - for r in Role.objects.filter(group=group).order_by('person__name').select_related("email", "person", "name"): - if r.name_id not in personnel: - personnel[r.name_id] = [] - personnel[r.name_id].append(r) - - if group.parent and group.parent.type_id == "area" and group.ad_role() and "ad" not in personnel: - ad_roles = list(Role.objects.filter(group=group.parent, name="ad", person=group.ad_role().person)) - if ad_roles: - personnel["ad"] = ad_roles - - group.personnel = [] - for role_name_slug, roles in personnel.items(): - label = roles[0].name.name - if len(roles) > 1: - if label.endswith("y"): - label = label[:-1] + "ies" - else: - label += "s" - - group.personnel.append((role_name_slug, label, roles)) - - group.personnel.sort(key=lambda t: t[2][0].name.order) - - milestone_state = "charter" if group.state_id == "proposed" else "active" - group.milestones = group.groupmilestone_set.filter(state=milestone_state) - if group.uses_milestone_dates: - group.milestones = group.milestones.order_by('resolved', 'due') - else: - group.milestones = group.milestones.order_by('resolved', 'order') - - if group.charter: - group.charter_text = get_charter_text(group) - else: - group.charter_text = "Not chartered yet." - group.charter_html = markdown.markdown(group.charter_text) - def extract_last_name(role): return role.person.name_parts()[3] -def fill_in_wg_roles(group): - def get_roles(slug, default): - for role_slug, label, roles in group.personnel: - if slug == role_slug: - return roles - return default - - group.chairs = get_roles("chair", []) - ads = get_roles("ad", []) - group.areadirector = ads[0] if ads else None - group.techadvisors = get_roles("techadv", []) - group.editors = get_roles("editor", []) - group.secretaries = get_roles("secr", []) - -def fill_in_wg_drafts(group): - group.drafts = Document.objects.filter(type_id="draft", group=group).order_by("name") - group.rfcs = Document.objects.filter(type_id="rfc", group=group).order_by("rfc_number") - for rfc in group.rfcs: - # TODO: remote_field? - rfc.remote_field = RelatedDocument.objects.filter(source=rfc,relationship_id__in=['obs','updates']).distinct() - rfc.invrel = RelatedDocument.objects.filter(target=rfc,relationship_id__in=['obs','updates']).distinct() - -def check_group_email_aliases(): - pattern = re.compile(r'expand-(.*?)(-\w+)@.*? +(.*)$') - tot_count = 0 - good_count = 0 - with io.open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file: - for line in virtual_file.readlines(): - m = pattern.match(line) - tot_count += 1 - if m: - good_count += 1 - if good_count > 50 and tot_count < 3*good_count: - return True - return False + +def response_from_file(fpath: Path) -> HttpResponse: + """Helper to shovel a file back in an HttpResponse""" + try: + content = fpath.read_bytes() + except IOError: + raise Http404 + return HttpResponse(content, content_type="text/plain; charset=utf-8") # --- View functions --------------------------------------------------- @@ -217,58 +150,26 @@ def check_group_email_aliases(): def wg_summary_area(request, group_type): if group_type != "wg": raise Http404 - areas = Group.objects.filter(type="area", state="active").order_by("name") - for area in areas: - area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym") - for group in area.groups: - group.chairs = sorted(roles(group, "chair"), key=extract_last_name) - - areas = [a for a in areas if a.groups] + return response_from_file(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt") - return render(request, 'group/1wg-summary.txt', - { 'areas': areas }, - content_type='text/plain; charset=UTF-8') def wg_summary_acronym(request, group_type): if group_type != "wg": raise Http404 - areas = Group.objects.filter(type="area", state="active").order_by("name") - groups = Group.objects.filter(type="wg", state="active").order_by("acronym").select_related("parent") - for group in groups: - group.chairs = sorted(roles(group, "chair"), key=extract_last_name) - return render(request, 'group/1wg-summary-by-acronym.txt', - { 'areas': areas, - 'groups': groups }, - content_type='text/plain; charset=UTF-8') + return response_from_file(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt") + -@cache_page ( 60 * 60, cache="slowpages" ) def wg_charters(request, group_type): if group_type != "wg": raise Http404 - areas = Group.objects.filter(type="area", state="active").order_by("name") - for area in areas: - area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("name") - for group in area.groups: - fill_in_charter_info(group) - fill_in_wg_roles(group) - fill_in_wg_drafts(group) - return render(request, 'group/1wg-charters.txt', - { 'areas': areas }, - content_type='text/plain; charset=UTF-8') - -@cache_page ( 60 * 60, cache="slowpages" ) + return response_from_file(Path(settings.CHARTER_PATH) / "1wg-charters.txt") + + def wg_charters_by_acronym(request, group_type): if group_type != "wg": raise Http404 + return response_from_file(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt") - groups = Group.objects.filter(type="wg", state="active").exclude(parent=None).order_by("acronym") - for group in groups: - fill_in_charter_info(group) - fill_in_wg_roles(group) - fill_in_wg_drafts(group) - return render(request, 'group/1wg-charters-by-acronym.txt', - { 'groups': groups }, - content_type='text/plain; charset=UTF-8') def active_groups(request, group_type=None): @@ -379,7 +280,7 @@ def active_wgs(request): if group.list_subscribe.startswith('http'): group.list_subscribe_url = group.list_subscribe elif group.list_email.endswith('@ietf.org'): - group.list_subscribe_url = MAILING_LIST_INFO_URL % {'list_addr':group.list_email.split('@')[0]} + group.list_subscribe_url = MAILING_LIST_INFO_URL % {'list_addr':group.list_email.split('@')[0].lower(),'domain':'ietf.org'} else: group.list_subscribe_url = "mailto:"+group.list_subscribe @@ -663,21 +564,6 @@ def group_about_status_edit(request, acronym, group_type=None): } ) -def get_group_email_aliases(acronym, group_type): - if acronym: - pattern = re.compile(r'expand-(%s)(-\w+)@.*? +(.*)$'%acronym) - else: - pattern = re.compile(r'expand-(.*?)(-\w+)@.*? +(.*)$') - - aliases = [] - with io.open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file: - for line in virtual_file.readlines(): - m = pattern.match(line) - if m: - if acronym or not group_type or Group.objects.filter(acronym=m.group(1),type__slug=group_type): - aliases.append({'acronym':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)}) - return aliases - def email(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) diff --git a/ietf/help/tests_views.py b/ietf/help/tests_views.py deleted file mode 100644 index ee80dad865..0000000000 --- a/ietf/help/tests_views.py +++ /dev/null @@ -1,21 +0,0 @@ -from pyquery import PyQuery - -from django.urls import reverse - -import debug # pyflakes:ignore - -from ietf.utils.test_utils import TestCase -from ietf.doc.models import StateType - -class HelpPageTests(TestCase): - - def test_state_index(self): - url = reverse('ietf.help.views.state_index') - r = self.client.get(url) - q = PyQuery(r.content) - content = [ e.text for e in q('#content table td a ') ] - names = StateType.objects.values_list('slug', flat=True) - # The following doesn't cover all doc types, only a selection - for name in names: - if not '-' in name: - self.assertIn(name, content) diff --git a/ietf/help/urls.py b/ietf/help/urls.py index f1cc625fa7..90ce7e12e9 100644 --- a/ietf/help/urls.py +++ b/ietf/help/urls.py @@ -2,10 +2,10 @@ from ietf.help import views from ietf.utils.urls import url +from django.views.generic import RedirectView urlpatterns = [ url(r'^state/(?P[-\w]+)/(?P[-\w]+)/?$', views.state), url(r'^state/(?P[-\w]+)/?$', views.state), - url(r'^state/?$', views.state_index), + url(r'^state/?$', RedirectView.as_view(url='/doc/help/state/', permanent=True)), ] - diff --git a/ietf/help/views.py b/ietf/help/views.py index 6b10f9f6c3..493bf0dcf1 100644 --- a/ietf/help/views.py +++ b/ietf/help/views.py @@ -1,23 +1,11 @@ # Copyright The IETF Trust 2007, All Rights Reserved -from django.shortcuts import get_object_or_404, render - import debug # pyflakes:ignore -from ietf.doc.models import State, StateType from ietf.name.models import StreamName +from django.shortcuts import redirect -def state_index(request): - types = StateType.objects.all() - names = [ type.slug for type in types ] - for type in types: - if "-" in type.slug and type.slug.split('-',1)[0] in names: - type.stategroups = None - else: - groups = StateType.objects.filter(slug__startswith=type.slug) - type.stategroups = [ g.slug[len(type.slug)+1:] for g in groups if not g == type ] or "" - - return render(request, 'help/state_index.html', {"types": types}) +# This is just a redirect to the new URL under /doc; can probably go away eventually. def state(request, doc, type=None): if type: @@ -25,6 +13,5 @@ def state(request, doc, type=None): if type in streams: type = "stream-%s" % type slug = "%s-%s" % (doc,type) if type else doc - statetype = get_object_or_404(StateType, slug=slug) - states = State.objects.filter(used=True, type=statetype).order_by('order') - return render(request, 'help/states.html', {"doc": doc, "type": statetype, "states":states} ) + return redirect('/doc/help/state/%s' % slug, permanent = True) + \ No newline at end of file diff --git a/ietf/idindex/tasks.py b/ietf/idindex/tasks.py index c01d50cf5d..5e7e193bba 100644 --- a/ietf/idindex/tasks.py +++ b/ietf/idindex/tasks.py @@ -2,6 +2,7 @@ # # Celery task definitions # +import os import shutil import debug # pyflakes:ignore @@ -10,6 +11,9 @@ from contextlib import AbstractContextManager from pathlib import Path from tempfile import NamedTemporaryFile +from typing import List + +from django.conf import settings from .index import all_id_txt, all_id2_txt, id_index_txt @@ -26,10 +30,14 @@ def make_temp_file(self, content): tf.write(content) return tf_path - def move_into_place(self, src_path: Path, dest_path: Path): + def move_into_place(self, src_path: Path, dest_path: Path, hardlink_dirs: List[Path] = []): shutil.move(src_path, dest_path) dest_path.chmod(0o644) self.cleanup_list.remove(src_path) + for path in hardlink_dirs: + target = path / dest_path.name + target.unlink(missing_ok=True) + os.link(dest_path, target) # until python>=3.10 def cleanup(self): for tf_path in self.cleanup_list: @@ -43,11 +51,13 @@ def __exit__(self, exc_type, exc_val, exc_tb): @shared_task def idindex_update_task(): """Update I-D indexes""" - id_path = Path("/a/ietfdata/doc/draft/repository") - derived_path = Path("/a/ietfdata/derived") - download_path = Path("/a/www/www6s/download") + id_path = Path(settings.INTERNET_DRAFT_PATH) + derived_path = Path(settings.DERIVED_DIR) + download_path = Path(settings.ALL_ID_DOWNLOAD_DIR) + ftp_path = Path(settings.FTP_DIR) / "internet-drafts" + all_archive_path = Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR) - with TempFileManager("/a/tmp") as tmp_mgr: + with TempFileManager() as tmp_mgr: # Generate copies of new contents all_id_content = all_id_txt() all_id_tmpfile = tmp_mgr.make_temp_file(all_id_content) @@ -69,17 +79,17 @@ def idindex_update_task(): derived_all_id2_tmpfile = tmp_mgr.make_temp_file(all_id2_content) # Move temp files as-atomically-as-possible into place - tmp_mgr.move_into_place(all_id_tmpfile, id_path / "all_id.txt") + tmp_mgr.move_into_place(all_id_tmpfile, id_path / "all_id.txt", [ftp_path, all_archive_path]) tmp_mgr.move_into_place(derived_all_id_tmpfile, derived_path / "all_id.txt") tmp_mgr.move_into_place(download_all_id_tmpfile, download_path / "id-all.txt") - tmp_mgr.move_into_place(id_index_tmpfile, id_path / "1id-index.txt") + tmp_mgr.move_into_place(id_index_tmpfile, id_path / "1id-index.txt", [ftp_path, all_archive_path]) tmp_mgr.move_into_place(derived_id_index_tmpfile, derived_path / "1id-index.txt") tmp_mgr.move_into_place(download_id_index_tmpfile, download_path / "id-index.txt") - tmp_mgr.move_into_place(id_abstracts_tmpfile, id_path / "1id-abstracts.txt") + tmp_mgr.move_into_place(id_abstracts_tmpfile, id_path / "1id-abstracts.txt", [ftp_path, all_archive_path]) tmp_mgr.move_into_place(derived_id_abstracts_tmpfile, derived_path / "1id-abstracts.txt") tmp_mgr.move_into_place(download_id_abstracts_tmpfile, download_path / "id-abstract.txt") - tmp_mgr.move_into_place(all_id2_tmpfile, id_path / "all_id2.txt") + tmp_mgr.move_into_place(all_id2_tmpfile, id_path / "all_id2.txt", [ftp_path, all_archive_path]) tmp_mgr.move_into_place(derived_all_id2_tmpfile, derived_path / "all_id2.txt") diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py index 31c3aaafbf..44abf805f0 100644 --- a/ietf/idindex/tests.py +++ b/ietf/idindex/tests.py @@ -188,17 +188,20 @@ def test_idindex_update_task( def test_temp_file_manager(self): with TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - with TempFileManager(temp_path) as tfm: - path1 = tfm.make_temp_file("yay") - path2 = tfm.make_temp_file("boo") # do not keep this one - self.assertTrue(path1.exists()) - self.assertTrue(path2.exists()) - dest = temp_path / "yay.txt" - tfm.move_into_place(path1, dest) - # make sure things were cleaned up... - self.assertFalse(path1.exists()) # moved to dest - self.assertFalse(path2.exists()) # left behind - # check destination contents and permissions - self.assertEqual(dest.read_text(), "yay") - self.assertEqual(dest.stat().st_mode & 0o777, 0o644) + with TemporaryDirectory() as other_dir: + temp_path = Path(temp_dir) + other_path = Path(other_dir) + with TempFileManager(temp_path) as tfm: + path1 = tfm.make_temp_file("yay") + path2 = tfm.make_temp_file("boo") # do not keep this one + self.assertTrue(path1.exists()) + self.assertTrue(path2.exists()) + dest = temp_path / "yay.txt" + tfm.move_into_place(path1, dest, [other_path]) + # make sure things were cleaned up... + self.assertFalse(path1.exists()) # moved to dest + self.assertFalse(path2.exists()) # left behind + # check destination contents and permissions + self.assertEqual(dest.read_text(), "yay") + self.assertEqual(dest.stat().st_mode & 0o777, 0o644) + self.assertTrue(dest.samefile(other_path / "yay.txt")) diff --git a/ietf/iesg/migrations/0003_delete_telechat.py b/ietf/iesg/migrations/0003_delete_telechat.py new file mode 100644 index 0000000000..6a09b88555 --- /dev/null +++ b/ietf/iesg/migrations/0003_delete_telechat.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.13 on 2024-06-21 20:40 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("iesg", "0002_telechatagendacontent"), + ] + + operations = [ + migrations.DeleteModel( + name="Telechat", + ), + ] diff --git a/ietf/iesg/models.py b/ietf/iesg/models.py index 96b78a195a..dcc8a9880b 100644 --- a/ietf/iesg/models.py +++ b/ietf/iesg/models.py @@ -59,20 +59,6 @@ def __str__(self): type_name = self.TYPE_CHOICES_DICT.get(self.type, str(self.type)) return "%s: %s" % (type_name, self.title or "") -class Telechat(models.Model): - telechat_id = models.IntegerField(primary_key=True) - telechat_date = models.DateField(null=True, blank=True) - minute_approved = models.IntegerField(null=True, blank=True) - wg_news_txt = models.TextField(blank=True) - iab_news_txt = models.TextField(blank=True) - management_issue = models.TextField(blank=True) - frozen = models.IntegerField(null=True, blank=True) - mi_frozen = models.IntegerField(null=True, blank=True) - - class Meta: - db_table = 'telechat' - - def next_telechat_date(): dates = TelechatDate.objects.order_by("-date") if dates: diff --git a/ietf/iesg/resources.py b/ietf/iesg/resources.py index c5deed27ff..c28dcf51d3 100644 --- a/ietf/iesg/resources.py +++ b/ietf/iesg/resources.py @@ -9,7 +9,7 @@ from ietf import api -from ietf.iesg.models import TelechatDate, Telechat, TelechatAgendaItem, TelechatAgendaContent +from ietf.iesg.models import TelechatDate, TelechatAgendaItem, TelechatAgendaContent class TelechatDateResource(ModelResource): @@ -17,62 +17,57 @@ class Meta: cache = SimpleCache() queryset = TelechatDate.objects.all() serializer = api.Serializer() - #resource_name = 'telechatdate' - ordering = ['id', ] - filtering = { + # resource_name = 'telechatdate' + ordering = [ + "id", + ] + filtering = { "id": ALL, "date": ALL, } + + api.iesg.register(TelechatDateResource()) -class TelechatResource(ModelResource): - class Meta: - cache = SimpleCache() - queryset = Telechat.objects.all() - serializer = api.Serializer() - #resource_name = 'telechat' - ordering = ['tlechat_id', ] - filtering = { - "telechat_id": ALL, - "telechat_date": ALL, - "minute_approved": ALL, - "wg_news_txt": ALL, - "iab_news_txt": ALL, - "management_issue": ALL, - "frozen": ALL, - "mi_frozen": ALL, - } -api.iesg.register(TelechatResource()) class TelechatAgendaItemResource(ModelResource): class Meta: cache = SimpleCache() queryset = TelechatAgendaItem.objects.all() serializer = api.Serializer() - #resource_name = 'telechatagendaitem' - ordering = ['id', ] - filtering = { + # resource_name = 'telechatagendaitem' + ordering = [ + "id", + ] + filtering = { "id": ALL, "text": ALL, "type": ALL, "title": ALL, } -api.iesg.register(TelechatAgendaItemResource()) +api.iesg.register(TelechatAgendaItemResource()) from ietf.name.resources import TelechatAgendaSectionNameResource + + class TelechatAgendaContentResource(ModelResource): - section = ToOneField(TelechatAgendaSectionNameResource, 'section') + section = ToOneField(TelechatAgendaSectionNameResource, "section") + class Meta: queryset = TelechatAgendaContent.objects.none() serializer = api.Serializer() cache = SimpleCache() - #resource_name = 'telechatagendacontent' - ordering = ['id', ] - filtering = { + # resource_name = 'telechatagendacontent' + ordering = [ + "id", + ] + filtering = { "id": ALL, "text": ALL, "section": ALL_WITH_RELATIONS, } + + api.iesg.register(TelechatAgendaContentResource()) diff --git a/ietf/ietfauth/htpasswd.py b/ietf/ietfauth/htpasswd.py deleted file mode 100644 index 3716d98600..0000000000 --- a/ietf/ietfauth/htpasswd.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import io -import subprocess, hashlib -from django.utils.encoding import force_bytes - -from django.conf import settings - -def update_htpasswd_file(username, password): - if getattr(settings, 'USE_PYTHON_HTDIGEST', None): - pass_file = settings.HTPASSWD_FILE - realm = settings.HTDIGEST_REALM - prefix = force_bytes('%s:%s:' % (username, realm)) - key = force_bytes(hashlib.md5(prefix + force_bytes(password)).hexdigest()) - f = io.open(pass_file, 'r+b') - pos = f.tell() - line = f.readline() - while line: - if line.startswith(prefix): - break - pos=f.tell() - line = f.readline() - f.seek(pos) - f.write(b'%s%s\n' % (prefix, key)) - f.close() - else: - p = subprocess.Popen([settings.HTPASSWD_COMMAND, "-b", settings.HTPASSWD_FILE, username, password], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() diff --git a/ietf/ietfauth/management/__init__.py b/ietf/ietfauth/management/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ietf/ietfauth/management/commands/__init__.py b/ietf/ietfauth/management/commands/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py b/ietf/ietfauth/management/commands/send_apikey_usage_emails.py deleted file mode 100644 index d3fce1bcc2..0000000000 --- a/ietf/ietfauth/management/commands/send_apikey_usage_emails.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright The IETF Trust 2017-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import datetime - -from textwrap import dedent - -from django.conf import settings -from django.core.management.base import BaseCommand -from django.utils import timezone - -import debug # pyflakes:ignore - -from ietf.person.models import PersonalApiKey, PersonApiKeyEvent -from ietf.utils.mail import send_mail - - -class Command(BaseCommand): - """ - Send out emails to all persons who have personal API keys about usage. - - Usage is show over the given period, where the default period is 7 days. - """ - - help = dedent(__doc__).strip() - - def add_arguments(self, parser): - parser.add_argument('-d', '--days', dest='days', type=int, default=7, - help='The period over which to show usage.') - - def handle(self, *filenames, **options): - """ - """ - - self.verbosity = int(options.get('verbosity')) - days = options.get('days') - - keys = PersonalApiKey.objects.filter(valid=True) - for key in keys: - earliest = timezone.now() - datetime.timedelta(days=days) - events = PersonApiKeyEvent.objects.filter(key=key, time__gt=earliest) - count = events.count() - events = events[:32] - if count: - key_name = key.hash()[:8] - subject = "API key usage for key '%s' for the last %s days" %(key_name, days) - to = key.person.email_address() - frm = settings.DEFAULT_FROM_EMAIL - send_mail(None, to, frm, subject, 'utils/apikey_usage_report.txt', {'person':key.person, - 'days':days, 'key':key, 'key_name':key_name, 'count':count, 'events':events, } ) - diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index 29afa56d78..6a85c6eb13 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -3,13 +3,10 @@ import datetime -import io import logging # pyflakes:ignore -import os import re import requests import requests_mock -import shutil import time import urllib @@ -21,7 +18,6 @@ from oic.utils.authn.client import CLIENT_AUTHN_METHOD from oidc_provider.models import RSAKey from pyquery import PyQuery -from unittest import skipIf from urllib.parse import urlsplit import django.core.signing @@ -35,78 +31,47 @@ from ietf.group.factories import GroupFactory, RoleFactory from ietf.group.models import Group, Role, RoleName -from ietf.ietfauth.htpasswd import update_htpasswd_file from ietf.ietfauth.utils import has_role from ietf.meeting.factories import MeetingFactory from ietf.nomcom.factories import NomComFactory from ietf.person.factories import PersonFactory, EmailFactory, UserFactory, PersonalApiKeyFactory from ietf.person.models import Person, Email, PersonalApiKey +from ietf.person.tasks import send_apikey_usage_emails_task from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory from ietf.review.models import ReviewWish, UnavailablePeriod from ietf.stats.models import MeetingRegistration -from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized from ietf.utils.timezone import date_today -import ietf.ietfauth.views - -if os.path.exists(settings.HTPASSWD_COMMAND): - skip_htpasswd_command = False - skip_message = "" -else: - skip_htpasswd_command = True - skip_message = ("Skipping htpasswd test: The binary for htpasswd wasn't found in the\n " - "location indicated in settings.py.") - print(" "+skip_message) - class IetfAuthTests(TestCase): - def setUp(self): - super().setUp() - self.saved_use_python_htdigest = getattr(settings, "USE_PYTHON_HTDIGEST", None) - settings.USE_PYTHON_HTDIGEST = True - - self.saved_htpasswd_file = settings.HTPASSWD_FILE - self.htpasswd_dir = self.tempdir('htpasswd') - settings.HTPASSWD_FILE = os.path.join(self.htpasswd_dir, "htpasswd") - io.open(settings.HTPASSWD_FILE, 'a').close() # create empty file - - self.saved_htdigest_realm = getattr(settings, "HTDIGEST_REALM", None) - settings.HTDIGEST_REALM = "test-realm" - - def tearDown(self): - shutil.rmtree(self.htpasswd_dir) - settings.USE_PYTHON_HTDIGEST = self.saved_use_python_htdigest - settings.HTPASSWD_FILE = self.saved_htpasswd_file - settings.HTDIGEST_REALM = self.saved_htdigest_realm - super().tearDown() def test_index(self): - self.assertEqual(self.client.get(urlreverse(ietf.ietfauth.views.index)).status_code, 200) + self.assertEqual(self.client.get(urlreverse("ietf.ietfauth.views.index")).status_code, 200) def test_login_and_logout(self): PersonFactory(user__username='plain') # try logging in without a next - r = self.client.get(urlreverse(ietf.ietfauth.views.login)) + r = self.client.get(urlreverse("ietf.ietfauth.views.login")) self.assertEqual(r.status_code, 200) - r = self.client.post(urlreverse(ietf.ietfauth.views.login), {"username":"plain", "password":"plain+password"}) + r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {"username":"plain", "password":"plain+password"}) self.assertEqual(r.status_code, 302) - self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile)) + self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.profile")) # try logging out r = self.client.post(urlreverse('django.contrib.auth.views.logout'), {}) self.assertEqual(r.status_code, 200) self.assertNotContains(r, "accounts/logout") - r = self.client.get(urlreverse(ietf.ietfauth.views.profile)) + r = self.client.get(urlreverse("ietf.ietfauth.views.profile")) self.assertEqual(r.status_code, 302) - self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.login)) + self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.login")) # try logging in with a next - r = self.client.post(urlreverse(ietf.ietfauth.views.login) + "?next=/foobar", {"username":"plain", "password":"plain+password"}) + r = self.client.post(urlreverse("ietf.ietfauth.views.login") + "?next=/foobar", {"username":"plain", "password":"plain+password"}) self.assertEqual(r.status_code, 302) self.assertEqual(urlsplit(r["Location"])[2], "/foobar") @@ -137,19 +102,19 @@ def _test_login(url): # try with a trivial next _test_login("/") # try with a next that requires login - _test_login(urlreverse(ietf.ietfauth.views.profile)) + _test_login(urlreverse("ietf.ietfauth.views.profile")) def test_login_with_different_email(self): person = PersonFactory(user__username='plain') email = EmailFactory(person=person) # try logging in without a next - r = self.client.get(urlreverse(ietf.ietfauth.views.login)) + r = self.client.get(urlreverse("ietf.ietfauth.views.login")) self.assertEqual(r.status_code, 200) - r = self.client.post(urlreverse(ietf.ietfauth.views.login), {"username":email, "password":"plain+password"}) + r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {"username":email, "password":"plain+password"}) self.assertEqual(r.status_code, 302) - self.assertEqual(urlsplit(r["Location"])[2], urlreverse(ietf.ietfauth.views.profile)) + self.assertEqual(urlsplit(r["Location"])[2], urlreverse("ietf.ietfauth.views.profile")) def extract_confirm_url(self, confirm_email): # dig out confirm_email link @@ -163,20 +128,11 @@ def extract_confirm_url(self, confirm_email): return confirm_url - def username_in_htpasswd_file(self, username): - with io.open(settings.HTPASSWD_FILE) as f: - for l in f: - if l.startswith(username + ":"): - return True - with io.open(settings.HTPASSWD_FILE) as f: - print(f.read()) - - return False # For the lowered barrier to account creation period, we are disabling this kind of failure # def test_create_account_failure(self): - # url = urlreverse(ietf.ietfauth.views.create_account) + # url = urlreverse("ietf.ietfauth.views.create_account") # # get # r = self.client.get(url) @@ -195,7 +151,7 @@ def test_create_account_failure_template(self): self.assertTrue("Additional Assistance Required" in r) def register(self, email): - url = urlreverse(ietf.ietfauth.views.create_account) + url = urlreverse("ietf.ietfauth.views.create_account") # register email empty_outbox() @@ -224,8 +180,6 @@ def register_and_verify(self, email): self.assertEqual(Person.objects.filter(user__username=email).count(), 1) self.assertEqual(Email.objects.filter(person__user__username=email).count(), 1) - self.assertTrue(self.username_in_htpasswd_file(email)) - # This also tests new account creation. def test_create_existing_account(self): @@ -240,7 +194,7 @@ def test_create_existing_account(self): note = get_payload_text(outbox[-1]) self.assertIn(email, note) self.assertIn("A datatracker account for that email already exists", note) - self.assertIn(urlreverse(ietf.ietfauth.views.password_reset), note) + self.assertIn(urlreverse("ietf.ietfauth.views.password_reset"), note) def test_ietfauth_profile(self): EmailFactory(person__user__username='plain') @@ -249,7 +203,7 @@ def test_ietfauth_profile(self): username = "plain" email_address = Email.objects.filter(person__user__username=username).first().address - url = urlreverse(ietf.ietfauth.views.profile) + url = urlreverse("ietf.ietfauth.views.profile") login_testing_unauthorized(self, username, url) @@ -400,7 +354,7 @@ def test_ietfauth_profile(self): def test_email_case_insensitive_protection(self): EmailFactory(address="TestAddress@example.net") person = PersonFactory() - url = urlreverse(ietf.ietfauth.views.profile) + url = urlreverse("ietf.ietfauth.views.profile") login_testing_unauthorized(self, person.user.username, url) data = { @@ -441,7 +395,7 @@ def test_nomcom_dressing_on_profile(self): def test_reset_password(self): - url = urlreverse(ietf.ietfauth.views.password_reset) + url = urlreverse("ietf.ietfauth.views.password_reset") email = 'someone@example.com' password = 'foobar' @@ -491,7 +445,6 @@ def test_reset_password(self): self.assertEqual(r.status_code, 200) q = PyQuery(r.content) self.assertEqual(len(q("form .is-invalid")), 0) - self.assertTrue(self.username_in_htpasswd_file(user.username)) # reuse reset url r = self.client.get(confirm_url) @@ -507,7 +460,7 @@ def test_reset_password(self): self.assertEqual(len(outbox), 1) confirm_url = self.extract_confirm_url(outbox[-1]) - r = self.client.post(urlreverse(ietf.ietfauth.views.login), {'username': email, 'password': password}) + r = self.client.post(urlreverse("ietf.ietfauth.views.login"), {'username': email, 'password': password}) r = self.client.get(confirm_url) self.assertEqual(r.status_code, 404) @@ -589,7 +542,7 @@ def test_review_overview(self): availability="unavailable", ) - url = urlreverse(ietf.ietfauth.views.review_overview) + url = urlreverse("ietf.ietfauth.views.review_overview") login_testing_unauthorized(self, reviewer.user.username, url) @@ -615,28 +568,10 @@ def test_review_overview(self): self.assertEqual(r.status_code, 302) self.assertEqual(ReviewWish.objects.filter(doc=doc, team=review_req.team).count(), 0) - def test_htpasswd_file_with_python(self): - # make sure we test both Python and call-out to binary - settings.USE_PYTHON_HTDIGEST = True - - update_htpasswd_file("foo", "passwd") - self.assertTrue(self.username_in_htpasswd_file("foo")) - - @skipIf(skip_htpasswd_command, skip_message) - @skip_coverage - def test_htpasswd_file_with_htpasswd_binary(self): - # make sure we test both Python and call-out to binary - settings.USE_PYTHON_HTDIGEST = False - - update_htpasswd_file("foo", "passwd") - self.assertTrue(self.username_in_htpasswd_file("foo")) - - def test_change_password(self): - - chpw_url = urlreverse(ietf.ietfauth.views.change_password) - prof_url = urlreverse(ietf.ietfauth.views.profile) - login_url = urlreverse(ietf.ietfauth.views.login) + chpw_url = urlreverse("ietf.ietfauth.views.change_password") + prof_url = urlreverse("ietf.ietfauth.views.profile") + login_url = urlreverse("ietf.ietfauth.views.login") redir_url = '%s?next=%s' % (login_url, chpw_url) # get without logging in @@ -681,9 +616,9 @@ def test_change_password(self): def test_change_username(self): - chun_url = urlreverse(ietf.ietfauth.views.change_username) - prof_url = urlreverse(ietf.ietfauth.views.profile) - login_url = urlreverse(ietf.ietfauth.views.login) + chun_url = urlreverse("ietf.ietfauth.views.change_username") + prof_url = urlreverse("ietf.ietfauth.views.profile") + login_url = urlreverse("ietf.ietfauth.views.login") redir_url = '%s?next=%s' % (login_url, chun_url) # get without logging in @@ -856,9 +791,6 @@ def test_apikey_errors(self): key2.delete() def test_send_apikey_report(self): - from ietf.ietfauth.management.commands.send_apikey_usage_emails import Command - from ietf.utils.mail import outbox, empty_outbox - person = RoleFactory(name_id='secr', group__acronym='secretariat').person url = urlreverse('ietf.ietfauth.views.apikey_create') @@ -883,9 +815,8 @@ def test_send_apikey_report(self): date = str(date_today()) empty_outbox() - cmd = Command() - cmd.handle(verbosity=0, days=7) - + send_apikey_usage_emails_task(days=7) + self.assertEqual(len(outbox), len(endpoints)) for mail in outbox: body = get_payload_text(mail) diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py index 30e639ad65..7493fe5c97 100644 --- a/ietf/ietfauth/urls.py +++ b/ietf/ietfauth/urls.py @@ -14,7 +14,7 @@ url(r'^confirmnewemail/(?P[^/]+)/$', views.confirm_new_email), url(r'^create/$', views.create_account), url(r'^create/confirm/(?P[^/]+)/$', views.confirm_account), - url(r'^login/$', views.login), + url(r'^login/$', views.AnyEmailLoginView.as_view(), name="ietf.ietfauth.views.login"), url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"), url(r'^password/$', views.change_password), url(r'^profile/$', views.profile), diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index 8c61b8356a..61c7b929b1 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -45,7 +45,7 @@ from django import forms from django.contrib import messages from django.conf import settings -from django.contrib.auth import update_session_auth_hash, logout, authenticate +from django.contrib.auth import logout, update_session_auth_hash from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.hashers import identify_hasher @@ -65,7 +65,6 @@ from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordForm, TestEmailForm, ChangePasswordForm, get_person_form, RoleEmailForm, NewEmailForm, ChangeUsernameForm, PersonPasswordForm) -from ietf.ietfauth.htpasswd import update_htpasswd_file from ietf.ietfauth.utils import has_role from ietf.name.models import ExtResourceName from ietf.nomcom.models import NomCom @@ -222,8 +221,6 @@ def confirm_account(request, auth): user = User.objects.create(username=email, email=email) user.set_password(password) user.save() - # password is also stored in htpasswd file - update_htpasswd_file(email, password) # make sure the rest of the person infrastructure is # well-connected @@ -552,8 +549,6 @@ def confirm_password_reset(request, auth): user.set_password(password) user.save() - # password is also stored in htpasswd file - update_htpasswd_file(user.username, password) success = True else: @@ -693,8 +688,6 @@ def change_password(request): user.set_password(new_password) user.save() - # password is also stored in htpasswd file - update_htpasswd_file(user.username, new_password) # keep the session update_session_auth_hash(request, user) @@ -731,13 +724,10 @@ def change_username(request): form = ChangeUsernameForm(user, request.POST) if form.is_valid(): new_username = form.cleaned_data["username"] - password = form.cleaned_data["password"] assert new_username in emails user.username = new_username.lower() user.save() - # password is also stored in htpasswd file - update_htpasswd_file(user.username, password) # keep the session update_session_auth_hash(request, user) @@ -752,53 +742,57 @@ def change_username(request): return render(request, 'registration/change_username.html', {'form': form}) - -def login(request, extra_context=None): - """ - This login function is a wrapper around django's login() for the purpose - of providing a notification if the user's password has been cleared. The - warning will be triggered if the password field has been set to something - which is not recognized as a valid password hash. +class AnyEmailAuthenticationForm(AuthenticationForm): + """AuthenticationForm that allows any email address as the username + + Also performs a check for a cleared password field and provides a helpful error message + if that applies to the user attempting to log in. """ - - if request.method == "POST": - form = AuthenticationForm(request, data=request.POST) - username = form.data.get('username') - user = User.objects.filter(username__iexact=username).first() # Consider _never_ actually looking for the User username and only looking at Email - if not user: - # try to find user ID from the email address + _unauthenticated_user = None + + def clean_username(self): + username = self.cleaned_data.get("username", None) + if username is None: + raise self.get_invalid_login_error() + user = User.objects.filter(username__iexact=username).first() + if user is None: email = Email.objects.filter(address=username).first() - if email and email.person and email.person.user: - u2 = email.person.user - # be conservative, only accept this if login is valid - if u2: - pw = form.data.get('password') - au = authenticate(request, username=u2.username, password=pw) - if au: - # kludge to change the querydict - q2 = request.POST.copy() - q2['username'] = u2.username - request.POST = q2 - user = u2 - # - if user: - try: - identify_hasher(user.password) + if email and email.person: + user = email.person.user # might be None + if user is None: + raise self.get_invalid_login_error() + self._unauthenticated_user = user # remember this for the clean() method + return user.username + + def clean(self): + if self._unauthenticated_user is not None: + try: + identify_hasher(self._unauthenticated_user.password) except ValueError: - extra_context = {"alert": - "Note: Your password has been cleared because " - "of possible password leakage. " - "Please use the password reset link below " - "to set a new password for your account.", - } - response = LoginView.as_view(extra_context=extra_context)(request) - if isinstance(response, HttpResponseRedirect) and user and user.is_authenticated: - try: - user.person - except Person.DoesNotExist: - logout(request) - response = render(request, 'registration/missing_person.html') - return response + self.add_error( + "password", + 'Your password has been cleared because of possible password leakage. ' + 'Please use the "Forgot your password?" button below to set a new password ' + 'for your account.', + ) + return super().clean() + + +class AnyEmailLoginView(LoginView): + """LoginView that allows any email address as the username + + Redirects to the missing_person page instead of logging in if the user does not have a Person + """ + form_class = AnyEmailAuthenticationForm + + def form_valid(self, form): + """Security check complete. Log the user in if they have a Person.""" + user = form.get_user() # user has authenticated at this point + if not hasattr(user, "person"): + logout(self.request) # should not be logged in yet, but just in case... + return render(self.request, "registration/missing_person.html") + return super().form_valid(form) + @login_required @person_required diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index 73b5d0dc5a..3a91c9dfb7 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -3,18 +3,20 @@ import datetime - +import mock from pyquery import PyQuery from urllib.parse import quote, urlparse from zoneinfo import ZoneInfo from django.conf import settings +from django.test.utils import override_settings from django.urls import reverse as urlreverse from django.utils import timezone import debug # pyflakes:ignore +from ietf.api.views import EmailIngestionError from ietf.doc.factories import ( DocumentFactory, WgDraftFactory, @@ -34,8 +36,9 @@ from ietf.ipr.models import (IprDisclosureBase,GenericIprDisclosure,HolderIprDisclosure, ThirdPartyIprDisclosure) from ietf.ipr.templatetags.ipr_filters import no_revisions_message -from ietf.ipr.utils import get_genitive, get_ipr_summary +from ietf.ipr.utils import get_genitive, get_ipr_summary, ingest_response_email from ietf.mailtrigger.utils import gather_address_lists +from ietf.message.factories import MessageFactory from ietf.message.models import Message from ietf.utils.mail import outbox, empty_outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized @@ -769,6 +772,36 @@ def test_process_response_email_uninteresting_with_invalid_encoding(self): result = process_response_email(message_bytes) self.assertIsNone(result) + @override_settings(ADMINS=(("Some Admin", "admin@example.com"),)) + @mock.patch("ietf.ipr.utils.process_response_email") + def test_ingest_response_email(self, mock_process_response_email): + message = b"What a nice message" + mock_process_response_email.side_effect = ValueError("ouch!") + with self.assertRaises(EmailIngestionError) as context: + ingest_response_email(message) + self.assertIsNone(context.exception.email_recipients) # default recipients + self.assertIsNotNone(context.exception.email_body) # body set + self.assertIsNotNone(context.exception.email_original_message) # original message attached + self.assertEqual(context.exception.email_attach_traceback, True) + self.assertTrue(mock_process_response_email.called) + self.assertEqual(mock_process_response_email.call_args, mock.call(message)) + mock_process_response_email.reset_mock() + + mock_process_response_email.side_effect = None + mock_process_response_email.return_value = None # rejected message + with self.assertRaises(EmailIngestionError) as context: + ingest_response_email(message) + self.assertIsNone(context.exception.as_emailmessage()) # should not send an email on a clean rejection + self.assertTrue(mock_process_response_email.called) + self.assertEqual(mock_process_response_email.call_args, mock.call(message)) + mock_process_response_email.reset_mock() + + # successful operation + mock_process_response_email.return_value = MessageFactory() + ingest_response_email(message) + self.assertTrue(mock_process_response_email.called) + self.assertEqual(mock_process_response_email.call_args, mock.call(message)) + def test_ajax_search(self): url = urlreverse('ietf.ipr.views.ajax_search') response=self.client.get(url+'?q=disclosure') diff --git a/ietf/ipr/utils.py b/ietf/ipr/utils.py index c4f17c4822..42d485ccad 100644 --- a/ietf/ipr/utils.py +++ b/ietf/ipr/utils.py @@ -1,6 +1,9 @@ # Copyright The IETF Trust 2014-2020, All Rights Reserved # -*- coding: utf-8 -*- +from textwrap import dedent + +from ietf.ipr.mail import process_response_email from ietf.ipr.models import IprDocRel import debug # pyflakes:ignore @@ -86,3 +89,25 @@ def generate_draft_recursive_txt(): f.write(data) +def ingest_response_email(message: bytes): + from ietf.api.views import EmailIngestionError # avoid circular import + try: + result = process_response_email(message) + except Exception as err: + # Message was rejected due to an unhandled exception. This is likely something + # the admins need to address, so send them a copy of the email. + raise EmailIngestionError( + "Datatracker IPR email ingestion error", + email_body=dedent("""\ + An error occurred while ingesting IPR email into the Datatracker. The original message is attached. + + {error_summary} + """), + email_original_message=message, + email_attach_traceback=True, + ) from err + + if result is None: + # Message was rejected due to some problem the sender can fix, so bounce but don't send + # an email to the admins + raise EmailIngestionError("IPR response rejected", email_body=None) diff --git a/ietf/liaisons/forms.py b/ietf/liaisons/forms.py index 0a6974e5bb..1d91041b25 100644 --- a/ietf/liaisons/forms.py +++ b/ietf/liaisons/forms.py @@ -32,7 +32,7 @@ from ietf.person.models import Email from ietf.person.fields import SearchableEmailField from ietf.doc.models import Document -from ietf.utils.fields import DatepickerDateField +from ietf.utils.fields import DatepickerDateField, ModelMultipleChoiceField from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO from functools import reduce @@ -200,7 +200,7 @@ def get_results(self): return results -class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField): +class CustomModelMultipleChoiceField(ModelMultipleChoiceField): '''If value is a QuerySet, return it as is (for use in widget.render)''' def prepare_value(self, value): if isinstance(value, QuerySetAny): @@ -215,12 +215,12 @@ def prepare_value(self, value): class LiaisonModelForm(forms.ModelForm): '''Specify fields which require a custom widget or that are not part of the model. ''' - from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False) + from_groups = ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False) from_groups.widget.attrs["class"] = "select2-field" from_groups.widget.attrs['data-minimum-input-length'] = 0 from_contact = forms.EmailField() # type: Union[forms.EmailField, SearchableEmailField] to_contacts = forms.CharField(label="Contacts", widget=forms.Textarea(attrs={'rows':'3', }), strip=False) - to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False) + to_groups = ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False) to_groups.widget.attrs["class"] = "select2-field" to_groups.widget.attrs['data-minimum-input-length'] = 0 deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True) diff --git a/ietf/mailinglists/admin.py b/ietf/mailinglists/admin.py index 51b906053f..081ee6477c 100644 --- a/ietf/mailinglists/admin.py +++ b/ietf/mailinglists/admin.py @@ -8,8 +8,8 @@ class NonWgMailingListAdmin(admin.ModelAdmin): - list_display = ('id', 'name', 'description') - search_fields = ('name',) + list_display = ('id', 'name', 'domain', 'description') + search_fields = ('name', 'domain') admin.site.register(NonWgMailingList, NonWgMailingListAdmin) diff --git a/ietf/mailinglists/factories.py b/ietf/mailinglists/factories.py index 1a3b0ffa1f..3be5770d76 100644 --- a/ietf/mailinglists/factories.py +++ b/ietf/mailinglists/factories.py @@ -11,6 +11,7 @@ class Meta: model = NonWgMailingList name = factory.Sequence(lambda n: "list-name-%s" % n) + domain = factory.Sequence(lambda n: "domain-%s.org" % n) description = factory.Faker('sentence', nb_words=10) diff --git a/ietf/mailinglists/migrations/0004_nonwgmailinglist_domain.py b/ietf/mailinglists/migrations/0004_nonwgmailinglist_domain.py new file mode 100644 index 0000000000..b977313a87 --- /dev/null +++ b/ietf/mailinglists/migrations/0004_nonwgmailinglist_domain.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.13 on 2024-06-05 17:51 + +from django.db import migrations, models +from django.db.models.functions import Lower + +IAB_NAMES = ["iab", "iab-stream"] +RFCED_NAMES = [ + "auth48archive", + "rfc-dist", + "rfc-editor-rfi", + "rfc-interest", + "rpat", + "rsab", +] +IRTF_NAMES = [ + "anrp-select", + "anrw-sc", + "anrw-tpc", + "crypto-panel", + "dtn-interest", + "irsg", + "irtf-announce", + "smart", + "teaching", + "travel-grants-commitee", +] + + +def forward(apps, schema_editor): + NonWgMailingList = apps.get_model("mailinglists", "NonWgMailingList") + NonWgMailingList.objects.annotate(lowername=Lower("name")).filter( + lowername__in=IAB_NAMES + ).update(domain="iab.org") + NonWgMailingList.objects.annotate(lowername=Lower("name")).filter( + lowername__in=IRTF_NAMES + ).update(domain="irtf.org") + NonWgMailingList.objects.annotate(lowername=Lower("name")).filter( + lowername__in=RFCED_NAMES + ).update(domain="rfc-editor.org") + + +def reverse(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ("mailinglists", "0003_remove_subscribed_lists_delete_list_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="nonwgmailinglist", + name="domain", + field=models.CharField(default="ietf.org", max_length=32), + ), + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/mailinglists/models.py b/ietf/mailinglists/models.py index f575ffe5a4..828d3823a4 100644 --- a/ietf/mailinglists/models.py +++ b/ietf/mailinglists/models.py @@ -14,12 +14,13 @@ # while decoupling from mailman2 until we integrate with mailman3 class NonWgMailingList(models.Model): name = models.CharField(max_length=32) + domain = models.CharField(max_length=32, default="ietf.org") description = models.CharField(max_length=256) def __str__(self): return "" % self.name def info_url(self): - return settings.MAILING_LIST_INFO_URL % {'list_addr': self.name } + return settings.MAILING_LIST_INFO_URL % {'list_addr': self.name.lower(), 'domain': self.domain.lower() } # Allowlisted is unused, but is not being dropped until its human-curated content # is archived outside this database. diff --git a/ietf/mailinglists/resources.py b/ietf/mailinglists/resources.py index b075d18079..4d1713b7b6 100644 --- a/ietf/mailinglists/resources.py +++ b/ietf/mailinglists/resources.py @@ -41,6 +41,7 @@ class Meta: filtering = { "id": ALL, "name": ALL, + "domain": ALL, "description": ALL, } api.mailinglists.register(NonWgMailingListResource()) diff --git a/ietf/mailinglists/tests.py b/ietf/mailinglists/tests.py index 0b44d28c71..8c5a550dfc 100644 --- a/ietf/mailinglists/tests.py +++ b/ietf/mailinglists/tests.py @@ -38,7 +38,9 @@ def test_nonwg(self): url = urlreverse("ietf.mailinglists.views.nonwg") r = self.client.get(url) + q = PyQuery(r.content) for l in lists: self.assertContains(r, l.name) self.assertContains(r, l.description) + self.assertNotEqual(q(f"a[href=\"{l.info_url()}\"]"), []) diff --git a/ietf/mailtrigger/utils.py b/ietf/mailtrigger/utils.py index d8b23ff056..9915eae3fd 100644 --- a/ietf/mailtrigger/utils.py +++ b/ietf/mailtrigger/utils.py @@ -149,9 +149,5 @@ def starts_with(prefix): return sorted(rule_list) -def get_base_submission_message_address(): - return Recipient.objects.get(slug="submission_manualpost_handling").gather()[0] - - def get_base_ipr_request_address(): return Recipient.objects.get(slug="ipr_requests").gather()[0] diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index 2cec669db9..b31ffb6cd7 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -28,7 +28,13 @@ from ietf.message.models import Message from ietf.name.models import TimeSlotTypeName, SessionPurposeName from ietf.person.models import Person -from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField, DatepickerSplitDateTimeWidget +from ietf.utils.fields import ( + DatepickerDateField, + DatepickerSplitDateTimeWidget, + DurationField, + ModelMultipleChoiceField, + MultiEmailField, +) from ietf.utils.validators import ( validate_file_size, validate_mime_type, validate_file_extension, validate_no_html_frame) @@ -551,7 +557,7 @@ class SwapTimeslotsForm(forms.Form): queryset=TimeSlot.objects.none(), # default to none, fill in when we have a meeting widget=forms.TextInput, ) - rooms = forms.ModelMultipleChoiceField( + rooms = ModelMultipleChoiceField( required=True, queryset=Room.objects.none(), # default to none, fill in when we have a meeting widget=CsvModelPkInput, @@ -617,7 +623,7 @@ class TimeSlotCreateForm(forms.Form): ) duration = TimeSlotDurationField() show_location = forms.BooleanField(required=False, initial=True) - locations = forms.ModelMultipleChoiceField( + locations = ModelMultipleChoiceField( queryset=Room.objects.none(), widget=forms.CheckboxSelectMultiple, ) diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index c0e250cdc0..7f1c85990e 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -1099,6 +1099,7 @@ def create_interim_session_conferences(sessions): try: confs = meetecho_manager.create( group=session.group, + session_id=session.pk, description=str(session), start_time=ts.utc_start_time(), duration=ts.duration, diff --git a/ietf/meeting/management/commands/import_iesg_minutes.py b/ietf/meeting/management/commands/import_iesg_minutes.py deleted file mode 100644 index d62e5058e4..0000000000 --- a/ietf/meeting/management/commands/import_iesg_minutes.py +++ /dev/null @@ -1,364 +0,0 @@ -# Copyright The IETF Trust 2023, All Rights Reserved - -from collections import namedtuple -import csv -import datetime -import os -import re -import shutil - -from django.conf import settings -from django.core.management import BaseCommand - -from pathlib import Path -from zoneinfo import ZoneInfo -from ietf.doc.models import DocEvent, Document - -from ietf.meeting.models import ( - Meeting, - SchedTimeSessAssignment, - Schedule, - SchedulingEvent, - Session, - TimeSlot, -) -from ietf.name.models import DocTypeName - - -def add_time_of_day(bare_datetime): - """Add a time for the iesg meeting based on a date and make it tzaware - - From the secretariat - the telechats happened at these times: - 2015-04-09 to present: 0700 PT America/Los Angeles - 1993-02-01 to 2015-03-12: 1130 ET America/New York - 1991-07-30 to 1993-01-25: 1200 ET America/New York - """ - dt = None - if bare_datetime.year > 2015: - dt = bare_datetime.replace(hour=7).replace( - tzinfo=ZoneInfo("America/Los_Angeles") - ) - elif bare_datetime.year == 2015: - if bare_datetime.month >= 4: - dt = bare_datetime.replace(hour=7).replace( - tzinfo=ZoneInfo("America/Los_Angeles") - ) - else: - dt = bare_datetime.replace(hour=11, minute=30).replace( - tzinfo=ZoneInfo("America/New_York") - ) - elif bare_datetime.year > 1993: - dt = bare_datetime.replace(hour=11, minute=30).replace( - tzinfo=ZoneInfo("America/New_York") - ) - elif bare_datetime.year == 1993: - if bare_datetime.month >= 2: - dt = bare_datetime.replace(hour=11, minute=30).replace( - tzinfo=ZoneInfo("America/New_York") - ) - else: - dt = bare_datetime.replace(hour=12).replace( - tzinfo=ZoneInfo("America/New_York") - ) - else: - dt = bare_datetime.replace(hour=12).replace(tzinfo=ZoneInfo("America/New_York")) - - return dt.astimezone(datetime.timezone.utc) - - -def build_bof_coord_data(): - CoordTuple = namedtuple("CoordTuple", "meeting_number source_name") - - def utc_from_la_time(time): - return time.replace(tzinfo=ZoneInfo("America/Los_Angeles")).astimezone( - datetime.timezone.utc - ) - - data = dict() - data[utc_from_la_time(datetime.datetime(2016, 6, 10, 7, 0))] = CoordTuple( - 96, "2015/bof-minutes-ietf-96.txt" - ) - data[utc_from_la_time(datetime.datetime(2016, 10, 6, 7, 0))] = CoordTuple( - 97, "2016/BoF-Minutes-2016-10-06.txt" - ) - data[utc_from_la_time(datetime.datetime(2017, 2, 15, 8, 0))] = CoordTuple( - 98, "2017/bof-minutes-ietf-98.txt" - ) - data[utc_from_la_time(datetime.datetime(2017, 6, 7, 8, 0))] = CoordTuple( - 99, "2017/bof-minutes-ietf-99.txt" - ) - data[utc_from_la_time(datetime.datetime(2017, 10, 5, 7, 0))] = CoordTuple( - 100, "2017/bof-minutes-ietf-100.txt" - ) - data[utc_from_la_time(datetime.datetime(2018, 2, 5, 11, 0))] = CoordTuple( - 101, "2018/bof-minutes-ietf-101.txt" - ) - data[utc_from_la_time(datetime.datetime(2018, 6, 5, 8, 0))] = CoordTuple( - 102, "2018/bof-minutes-ietf-102.txt" - ) - data[utc_from_la_time(datetime.datetime(2018, 9, 26, 7, 0))] = CoordTuple( - 103, "2018/bof-minutes-ietf-103.txt" - ) - data[utc_from_la_time(datetime.datetime(2019, 2, 15, 9, 0))] = CoordTuple( - 104, "2019/bof-minutes-ietf-104.txt" - ) - data[utc_from_la_time(datetime.datetime(2019, 6, 11, 7, 30))] = CoordTuple( - 105, "2019/bof-minutes-ietf-105.txt" - ) - data[utc_from_la_time(datetime.datetime(2019, 10, 9, 6, 30))] = CoordTuple( - 106, "2019/bof-minutes-ietf-106.txt" - ) - data[utc_from_la_time(datetime.datetime(2020, 2, 13, 8, 0))] = CoordTuple( - 107, "2020/bof-minutes-ietf-107.txt" - ) - data[utc_from_la_time(datetime.datetime(2020, 6, 15, 8, 0))] = CoordTuple( - 108, "2020/bof-minutes-ietf-108.txt" - ) - data[utc_from_la_time(datetime.datetime(2020, 10, 9, 7, 0))] = CoordTuple( - 109, "2020/bof-minutes-ietf-109.txt" - ) - data[utc_from_la_time(datetime.datetime(2021, 1, 14, 13, 30))] = CoordTuple( - 110, "2021/bof-minutes-ietf-110.txt" - ) - data[utc_from_la_time(datetime.datetime(2021, 6, 1, 8, 0))] = CoordTuple( - 111, "2021/bof-minutes-ietf-111.txt" - ) - data[utc_from_la_time(datetime.datetime(2021, 9, 15, 9, 0))] = CoordTuple( - 112, "2021/bof-minutes-ietf-112.txt" - ) - data[utc_from_la_time(datetime.datetime(2022, 1, 28, 7, 0))] = CoordTuple( - 113, "2022/bof-minutes-ietf-113.txt" - ) - data[utc_from_la_time(datetime.datetime(2022, 6, 2, 10, 0))] = CoordTuple( - 114, "2022/bof-minutes-ietf-114.txt" - ) - data[utc_from_la_time(datetime.datetime(2022, 9, 13, 9, 0))] = CoordTuple( - 115, "2022/bof-minutes-ietf-115.txt" - ) - data[utc_from_la_time(datetime.datetime(2023, 2, 1, 9, 0))] = CoordTuple( - 116, "2023/bof-minutes-ietf-116.txt" - ) - data[utc_from_la_time(datetime.datetime(2023, 6, 1, 7, 0))] = CoordTuple( - 117, "2023/bof-minutes-ietf-117.txt" - ) - data[utc_from_la_time(datetime.datetime(2023, 9, 15, 8, 0))] = CoordTuple( - 118, "2023/bof-minutes-ietf-118.txt" - ) - return data - - -class Command(BaseCommand): - help = "Performs a one-time import of IESG minutes, creating Meetings to attach them to" - - def handle(self, *args, **options): - old_minutes_root = ( - "/a/www/www6/iesg/minutes" - if settings.SERVER_MODE == "production" - else "/assets/www6/iesg/minutes" - ) - minutes_dir = Path(old_minutes_root) - date_re = re.compile(r"\d{4}-\d{2}-\d{2}") - meeting_times = set() - redirects = [] - for file_prefix in ["minutes", "narrative"]: - paths = list(minutes_dir.glob(f"[12][09][0129][0-9]/{file_prefix}*.txt")) - paths.extend( - list(minutes_dir.glob(f"[12][09][0129][0-9]/{file_prefix}*.html")) - ) - for path in paths: - s = date_re.search(path.name) - if s: - meeting_times.add( - add_time_of_day( - datetime.datetime.strptime(s.group(), "%Y-%m-%d") - ) - ) - bof_coord_data = build_bof_coord_data() - bof_times = set(bof_coord_data.keys()) - assert len(bof_times.intersection(meeting_times)) == 0 - meeting_times.update(bof_times) - year_seen = None - for dt in sorted(meeting_times): - if dt.year != year_seen: - counter = 1 - year_seen = dt.year - meeting_name = f"interim-{dt.year}-iesg-{counter:02d}" - meeting = Meeting.objects.create( - number=meeting_name, - type_id="interim", - date=dt.date(), - days=1, - time_zone=dt.tzname(), - ) - schedule = Schedule.objects.create( - meeting=meeting, - owner_id=1, # the "(System)" person - visible=True, - public=True, - ) - meeting.schedule = schedule - meeting.save() - session = Session.objects.create( - meeting=meeting, - group_id=2, # The IESG group - type_id="regular", - purpose_id="regular", - name=( - f"IETF {bof_coord_data[dt].meeting_number} BOF Coordination Call" - if dt in bof_times - else "Formal Telechat" - ), - ) - SchedulingEvent.objects.create( - session=session, - status_id="sched", - by_id=1, # (System) - ) - timeslot = TimeSlot.objects.create( - meeting=meeting, - type_id="regular", - time=dt, - duration=datetime.timedelta(seconds=2 * 60 * 60), - ) - SchedTimeSessAssignment.objects.create( - timeslot=timeslot, session=session, schedule=schedule - ) - - if dt in bof_times: - source = minutes_dir / bof_coord_data[dt].source_name - if source.exists(): - doc_name = ( - f"minutes-interim-{dt.year}-iesg-{counter:02d}-{dt:%Y%m%d%H%M}" - ) - doc_filename = f"{doc_name}-00.txt" - doc = Document.objects.create( - name=doc_name, - type_id="minutes", - title=f"Minutes IETF {bof_coord_data[dt].meeting_number} BOF coordination {meeting_name} {dt:%Y-%m-%d %H:%M}", - group_id=2, # the IESG group - rev="00", - uploaded_filename=doc_filename, - ) - e = DocEvent.objects.create( - type="comment", - doc=doc, - rev="00", - by_id=1, # "(System)" - desc="Minutes moved into datatracker", - ) - doc.save_with_history([e]) - session.presentations.create(document=doc, rev=doc.rev) - dest = ( - Path(settings.AGENDA_PATH) - / meeting_name - / "minutes" - / doc_filename - ) - if dest.exists(): - self.stdout.write( - f"WARNING: {dest} already exists - not overwriting it." - ) - else: - os.makedirs(dest.parent, exist_ok=True) - shutil.copy(source, dest) - redirects.append( - [ - f"www6.ietf.org/iesg/minutes/{dt.year}/{bof_coord_data[dt].source_name}", - f"https://datatracker.ietf.org/doc/{doc_name}", - 302, - ] - ) - else: - for type_id in ["minutes", "narrativeminutes"]: - source_file_prefix = ( - "minutes" if type_id == "minutes" else "narrative-minutes" - ) - txt_source = ( - minutes_dir - / f"{dt.year}" - / f"{source_file_prefix}-{dt:%Y-%m-%d}.txt" - ) - html_source = ( - minutes_dir - / f"{dt.year}" - / f"{source_file_prefix}-{dt:%Y-%m-%d}.html" - ) - if txt_source.exists() and html_source.exists(): - self.stdout.write( - f"WARNING: Both {txt_source} and {html_source} exist." - ) - if txt_source.exists() or html_source.exists(): - prefix = DocTypeName.objects.get(slug=type_id).prefix - doc_name = f"{prefix}-interim-{dt.year}-iesg-{counter:02d}-{dt:%Y%m%d%H%M}" - suffix = "html" if html_source.exists() else "txt" - doc_filename = f"{doc_name}-00.{suffix}" - verbose_type = ( - "Minutes" if type_id == "minutes" else "Narrative Minutes" - ) - doc = Document.objects.create( - name=doc_name, - type_id=type_id, - title=f"{verbose_type} {meeting_name} {dt:%Y-%m-%d %H:%M}", - group_id=2, # the IESG group - rev="00", - uploaded_filename=doc_filename, - ) - e = DocEvent.objects.create( - type="comment", - doc=doc, - rev="00", - by_id=1, # "(System)" - desc=f"{verbose_type} moved into datatracker", - ) - doc.save_with_history([e]) - session.presentations.create(document=doc, rev=doc.rev) - dest = ( - Path(settings.AGENDA_PATH) - / meeting_name - / type_id - / doc_filename - ) - if dest.exists(): - self.stdout.write( - f"WARNING: {dest} already exists - not overwriting it." - ) - else: - os.makedirs(dest.parent, exist_ok=True) - if html_source.exists(): - html_content = html_source.read_text(encoding="utf-8") - html_content = html_content.replace( - f'href="IESGnarrative-{dt:%Y-%m-%d}.html#', - 'href="#', - ) - html_content = re.sub( - r']*>([^<]*)', - r"\1", - html_content, - ) - html_content = re.sub( - r'([^<]*)', - r"\1", - html_content, - ) - html_content = re.sub( - ' 0 and days_passed % nomcom.reminder_interval == 0 - else: - return bool(nomcom.reminderdates_set.filter(date=send_date)) - -class Command(BaseCommand): - help = ("Send acceptance and questionnaire reminders to nominees") - - def handle(self, *args, **options): - for nomcom in NomCom.objects.filter(group__state__slug='active'): - nps = NomineePosition.objects.filter(nominee__nomcom=nomcom,nominee__duplicated__isnull=True) - for nominee_position in nps.pending(): - if is_time_to_send(nomcom, date_today(), nominee_position.time.date()): - send_accept_reminder_to_nominee(nominee_position) - log('Sent accept reminder to %s' % nominee_position.nominee.email.address) - for nominee_position in nps.accepted().without_questionnaire_response(): - if is_time_to_send(nomcom, date_today(), nominee_position.time.date()): - send_questionnaire_reminder_to_nominee(nominee_position) - log('Sent questionnaire reminder to %s' % nominee_position.nominee.email.address) diff --git a/ietf/nomcom/tasks.py b/ietf/nomcom/tasks.py new file mode 100644 index 0000000000..3d063a6b26 --- /dev/null +++ b/ietf/nomcom/tasks.py @@ -0,0 +1,10 @@ +# Copyright The IETF Trust 2024, All Rights Reserved + +from celery import shared_task + +from .utils import send_reminders + + +@shared_task +def send_nomcom_reminders_task(): + send_reminders() diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index cdbb860c43..8f94cc7fc5 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -24,6 +24,7 @@ import debug # pyflakes:ignore +from ietf.api.views import EmailIngestionError from ietf.dbtemplate.factories import DBTemplateFactory from ietf.dbtemplate.models import DBTemplate from ietf.doc.factories import DocEventFactory, WgDocumentAuthorFactory, \ @@ -37,15 +38,16 @@ MEMBER_USER, SECRETARIAT_USER, EMAIL_DOMAIN, NOMCOM_YEAR from ietf.nomcom.models import NomineePosition, Position, Nominee, \ NomineePositionStateName, Feedback, FeedbackTypeName, \ - Nomination, FeedbackLastSeen, TopicFeedbackLastSeen, ReminderDates -from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send + Nomination, FeedbackLastSeen, TopicFeedbackLastSeen, ReminderDates, \ + NomCom from ietf.nomcom.factories import NomComFactory, FeedbackFactory, TopicFactory, \ nomcom_kwargs_for_year, provide_private_key_to_test_client, \ key +from ietf.nomcom.tasks import send_nomcom_reminders_task from ietf.nomcom.utils import get_nomcom_by_year, make_nomineeposition, \ get_hash_nominee_position, is_eligible, list_eligible, \ - get_eligibility_date, suggest_affiliation, \ - decorate_volunteers_with_qualifications + get_eligibility_date, suggest_affiliation, ingest_feedback_email, \ + decorate_volunteers_with_qualifications, send_reminders, _is_time_to_send_reminder from ietf.person.factories import PersonFactory, EmailFactory from ietf.person.models import Email, Person from ietf.stats.models import MeetingRegistration @@ -1114,6 +1116,47 @@ def test_encrypted_comments(self): self.assertNotEqual(feedback.comments, comment_text) self.assertEqual(check_comments(feedback.comments, comment_text, self.privatekey_file), True) + @mock.patch("ietf.nomcom.utils.create_feedback_email") + def test_ingest_feedback_email(self, mock_create_feedback_email): + message = b"This is nomcom feedback" + no_nomcom_year = date_today().year + 10 # a guess at a year with no nomcoms + while NomCom.objects.filter(group__acronym__icontains=no_nomcom_year).exists(): + no_nomcom_year += 1 + inactive_nomcom = NomComFactory(group__state_id="conclude", group__acronym=f"nomcom{no_nomcom_year + 1}") + + # cases where the nomcom does not exist, so admins are notified + for bad_year in (no_nomcom_year, inactive_nomcom.year()): + with self.assertRaises(EmailIngestionError) as context: + ingest_feedback_email(message, bad_year) + self.assertIn("does not exist", context.exception.msg) + self.assertIsNotNone(context.exception.email_body) # error message to be sent + self.assertIsNone(context.exception.email_recipients) # default recipients (i.e., admin) + self.assertIsNone(context.exception.email_original_message) # no original message + self.assertFalse(context.exception.email_attach_traceback) # no traceback + self.assertFalse(mock_create_feedback_email.called) + + # nomcom exists but an error occurs, so feedback goes to the nomcom chair + active_nomcom = NomComFactory(group__acronym=f"nomcom{no_nomcom_year + 2}") + mock_create_feedback_email.side_effect = ValueError("ouch!") + with self.assertRaises(EmailIngestionError) as context: + ingest_feedback_email(message, active_nomcom.year()) + self.assertIn(f"Error ingesting nomcom {active_nomcom.year()}", context.exception.msg) + self.assertIsNotNone(context.exception.email_body) # error message to be sent + self.assertEqual(context.exception.email_recipients, active_nomcom.chair_emails()) + self.assertEqual(context.exception.email_original_message, message) + self.assertFalse(context.exception.email_attach_traceback) # no traceback + self.assertTrue(mock_create_feedback_email.called) + self.assertEqual(mock_create_feedback_email.call_args, mock.call(active_nomcom, message)) + mock_create_feedback_email.reset_mock() + + # and, finally, success + mock_create_feedback_email.side_effect = None + mock_create_feedback_email.return_value = FeedbackFactory(author="someone@example.com") + ingest_feedback_email(message, active_nomcom.year()) + self.assertTrue(mock_create_feedback_email.called) + self.assertEqual(mock_create_feedback_email.call_args, mock.call(active_nomcom, message)) + + class ReminderTest(TestCase): def setUp(self): @@ -1164,36 +1207,41 @@ def tearDown(self): teardown_test_public_keys_dir(self) super().tearDown() - def test_is_time_to_send(self): + def test_is_time_to_send_reminder(self): self.nomcom.reminder_interval = 4 today = date_today() - self.assertTrue(is_time_to_send(self.nomcom,today+datetime.timedelta(days=4),today)) + self.assertTrue( + _is_time_to_send_reminder(self.nomcom, today + datetime.timedelta(days=4), today) + ) for delta in range(4): - self.assertFalse(is_time_to_send(self.nomcom,today+datetime.timedelta(days=delta),today)) + self.assertFalse( + _is_time_to_send_reminder( + self.nomcom, today + datetime.timedelta(days=delta), today + ) + ) self.nomcom.reminder_interval = None - self.assertFalse(is_time_to_send(self.nomcom,today,today)) + self.assertFalse(_is_time_to_send_reminder(self.nomcom, today, today)) self.nomcom.reminderdates_set.create(date=today) - self.assertTrue(is_time_to_send(self.nomcom,today,today)) + self.assertTrue(_is_time_to_send_reminder(self.nomcom, today, today)) - def test_command(self): - c = Command() - messages_before=len(outbox) + def test_send_reminders(self): + messages_before = len(outbox) self.nomcom.reminder_interval = 3 self.nomcom.save() - c.handle(None,None) + send_reminders() self.assertEqual(len(outbox), messages_before + 2) self.assertIn('nominee1@example.org', outbox[-1]['To']) self.assertIn('please complete', outbox[-1]['Subject']) self.assertIn('nominee1@example.org', outbox[-2]['To']) self.assertIn('please accept', outbox[-2]['Subject']) - messages_before=len(outbox) + messages_before = len(outbox) self.nomcom.reminder_interval = 4 self.nomcom.save() - c.handle(None,None) + send_reminders() self.assertEqual(len(outbox), messages_before + 1) self.assertIn('nominee2@example.org', outbox[-1]['To']) self.assertIn('please accept', outbox[-1]['Subject']) - + def test_remind_accept_view(self): url = reverse('ietf.nomcom.views.send_reminder_mail', kwargs={'year': NOMCOM_YEAR,'type':'accept'}) login_testing_unauthorized(self, CHAIR_USER, url) @@ -3005,3 +3053,10 @@ def test_reclassify_feedback_unrelated(self): self.assertEqual(fb.type_id, 'junk') self.assertEqual(Feedback.objects.filter(type='read').count(), 0) self.assertEqual(Feedback.objects.filter(type='junk').count(), 1) + + +class TaskTests(TestCase): + @mock.patch("ietf.nomcom.tasks.send_reminders") + def test_send_nomcom_reminders_task(self, mock_send): + send_nomcom_reminders_task() + self.assertEqual(mock_send.call_count, 1) diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 23e89026d1..ab155ef1d5 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -16,6 +16,7 @@ from email.header import decode_header from email.iterators import typed_subpart_iterator from email.utils import parseaddr +from textwrap import dedent from django.db.models import Q, Count from django.conf import settings @@ -715,3 +716,58 @@ def extract_volunteers(year): decorate_volunteers_with_qualifications(volunteers,nomcom=nomcom) volunteers = sorted(volunteers,key=lambda v:(not v.eligible,v.person.last_name())) return nomcom, volunteers + + +def ingest_feedback_email(message: bytes, year: int): + from ietf.api.views import EmailIngestionError # avoid circular import + from .models import NomCom + try: + nomcom = NomCom.objects.get(group__acronym__icontains=str(year), + group__state__slug='active') + except NomCom.DoesNotExist: + raise EmailIngestionError( + f"Error ingesting nomcom email: nomcom {year} does not exist or is not active", + email_body=dedent(f"""\ + An email for nomcom {year} was posted to ingest_feedback_email, but no + active nomcom exists for that year. + """), + ) + + try: + feedback = create_feedback_email(nomcom, message) + except Exception as err: + raise EmailIngestionError( + f"Error ingesting nomcom {year} feedback email", + email_recipients=nomcom.chair_emails(), + email_body=dedent(f"""\ + An error occurred while ingesting feedback email for nomcom {year}. + + {{error_summary}} + """), + email_original_message=message, + ) from err + log("Received nomcom email from %s" % feedback.author) + + +def _is_time_to_send_reminder(nomcom, send_date, nomination_date): + if nomcom.reminder_interval: + days_passed = (send_date - nomination_date).days + return days_passed > 0 and days_passed % nomcom.reminder_interval == 0 + else: + return bool(nomcom.reminderdates_set.filter(date=send_date)) + + +def send_reminders(): + from .models import NomCom, NomineePosition + for nomcom in NomCom.objects.filter(group__state__slug="active"): + nps = NomineePosition.objects.filter( + nominee__nomcom=nomcom, nominee__duplicated__isnull=True + ) + for nominee_position in nps.pending(): + if _is_time_to_send_reminder(nomcom, date_today(), nominee_position.time.date()): + send_accept_reminder_to_nominee(nominee_position) + log(f"Sent accept reminder to {nominee_position.nominee.email.address}") + for nominee_position in nps.accepted().without_questionnaire_response(): + if _is_time_to_send_reminder(nomcom, date_today(), nominee_position.time.date()): + send_questionnaire_reminder_to_nominee(nominee_position) + log(f"Sent questionnaire reminder to {nominee_position.nominee.email.address}") diff --git a/ietf/person/management/commands/purge_old_personal_api_key_events.py b/ietf/person/management/commands/purge_old_personal_api_key_events.py deleted file mode 100644 index 66b9d2c33e..0000000000 --- a/ietf/person/management/commands/purge_old_personal_api_key_events.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright The IETF Trust 2021, All Rights Reserved -# -*- coding: utf-8 -*- - -from datetime import timedelta -from django.core.management.base import BaseCommand, CommandError -from django.db.models import Max, Min -from django.utils import timezone - -from ietf.person.models import PersonApiKeyEvent - - -class Command(BaseCommand): - help = 'Purge PersonApiKeyEvent instances older than KEEP_DAYS days' - - def add_arguments(self, parser): - parser.add_argument('keep_days', type=int, - help='Delete events older than this many days') - parser.add_argument('-n', '--dry-run', action='store_true', default=False, - help="Don't delete events, just show what would be done") - - - def handle(self, *args, **options): - keep_days = options['keep_days'] - dry_run = options['dry_run'] - verbosity = options.get("verbosity", 1) - - def _format_count(count, unit='day'): - return '{} {}{}'.format(count, unit, ('' if count == 1 else 's')) - - if keep_days < 0: - raise CommandError('Negative keep_days not allowed ({} was specified)'.format(keep_days)) - - if verbosity > 1: - self.stdout.write('purge_old_personal_api_key_events: Finding events older than {}\n'.format(_format_count(keep_days))) - if dry_run: - self.stdout.write('Dry run requested, records will not be deleted\n') - self.stdout.flush() - - now = timezone.now() - old_events = PersonApiKeyEvent.objects.filter( - time__lt=now - timedelta(days=keep_days) - ) - - stats = old_events.aggregate(Min('time'), Max('time')) - old_count = old_events.count() - if old_count == 0: - if verbosity > 1: - self.stdout.write('No events older than {} found\n'.format(_format_count(keep_days))) - return - - oldest_date = stats['time__min'] - oldest_ago = now - oldest_date - newest_date = stats['time__max'] - newest_ago = now - newest_date - - action_fmt = 'Would delete {}\n' if dry_run else 'Deleting {}\n' - if verbosity > 1: - self.stdout.write(action_fmt.format(_format_count(old_count, 'event'))) - self.stdout.write(' Oldest at {} ({} ago)\n'.format(oldest_date, _format_count(oldest_ago.days))) - self.stdout.write(' Most recent at {} ({} ago)\n'.format(newest_date, _format_count(newest_ago.days))) - self.stdout.flush() - - if not dry_run: - old_events.delete() diff --git a/ietf/person/management/commands/tests.py b/ietf/person/management/commands/tests.py deleted file mode 100644 index 38d770a588..0000000000 --- a/ietf/person/management/commands/tests.py +++ /dev/null @@ -1,122 +0,0 @@ -# Copyright The IETF Trust 2021, All Rights Reserved -# -*- coding: utf-8 -*- - -import datetime -from io import StringIO - -from django.core.management import call_command, CommandError -from django.utils import timezone - -from ietf.person.factories import PersonApiKeyEventFactory -from ietf.person.models import PersonApiKeyEvent, PersonEvent -from ietf.utils.test_utils import TestCase - - -class CommandTests(TestCase): - @staticmethod - def _call_command(command_name, *args, **options): - out = StringIO() - options['stdout'] = out - call_command(command_name, *args, **options) - return out.getvalue() - - def _assert_purge_results(self, cmd_output, expected_delete_count, expected_kept_events): - self.assertNotIn('Dry run requested', cmd_output) - if expected_delete_count == 0: - delete_text = 'No events older than' - else: - delete_text = 'Deleting {} event'.format(expected_delete_count) - self.assertIn(delete_text, cmd_output) - self.assertCountEqual( - PersonApiKeyEvent.objects.all(), - expected_kept_events, - 'Wrong events were deleted' - ) - - def _assert_purge_dry_run_results(self, cmd_output, expected_delete_count, expected_kept_events): - self.assertIn('Dry run requested', cmd_output) - if expected_delete_count == 0: - delete_text = 'No events older than' - else: - delete_text = 'Would delete {} event'.format(expected_delete_count) - self.assertIn(delete_text, cmd_output) - self.assertCountEqual( - PersonApiKeyEvent.objects.all(), - expected_kept_events, - 'Events were deleted when dry-run option was used' - ) - - def test_purge_old_personal_api_key_events(self): - keep_days = 10 - - # Remember how many PersonEvents were present so we can verify they're cleaned up properly. - personevents_before = PersonEvent.objects.count() - - now = timezone.now() - # The first of these events will be timestamped a fraction of a second more than keep_days - # days ago by the time we call the management command, so will just barely chosen for purge. - old_events = [ - PersonApiKeyEventFactory(time=now - datetime.timedelta(days=n)) - for n in range(keep_days, 2 * keep_days + 1) - ] - num_old_events = len(old_events) - - recent_events = [ - PersonApiKeyEventFactory(time=now - datetime.timedelta(days=n)) - for n in range(0, keep_days) - ] - # We did not create recent_event timestamped exactly keep_days ago because it would - # be treated as an old_event by the management command. Create an event a few seconds - # on the "recent" side of keep_days old to test the threshold. - recent_events.append( - PersonApiKeyEventFactory( - time=now + datetime.timedelta(seconds=3) - datetime.timedelta(days=keep_days) - ) - ) - num_recent_events = len(recent_events) - - # call with dry run - output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '--dry-run', '-v2') - self._assert_purge_dry_run_results(output, num_old_events, old_events + recent_events) - - # call for real - output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '-v2') - self._assert_purge_results(output, num_old_events, recent_events) - self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events, - 'PersonEvents were not cleaned up properly') - - # repeat - there should be nothing left to delete - output = self._call_command('purge_old_personal_api_key_events', '--dry-run', str(keep_days), '-v2') - self._assert_purge_dry_run_results(output, 0, recent_events) - - output = self._call_command('purge_old_personal_api_key_events', str(keep_days), '-v2') - self._assert_purge_results(output, 0, recent_events) - self.assertEqual(PersonEvent.objects.count(), personevents_before + num_recent_events, - 'PersonEvents were not cleaned up properly') - - # and now delete the remaining events - output = self._call_command('purge_old_personal_api_key_events', '0', '-v2') - self._assert_purge_results(output, num_recent_events, []) - self.assertEqual(PersonEvent.objects.count(), personevents_before, - 'PersonEvents were not cleaned up properly') - - def test_purge_old_personal_api_key_events_rejects_invalid_arguments(self): - """The purge_old_personal_api_key_events command should reject invalid arguments""" - event = PersonApiKeyEventFactory(time=timezone.now() - datetime.timedelta(days=30)) - - with self.assertRaises(CommandError): - self._call_command('purge_old_personal_api_key_events') - - with self.assertRaises(CommandError): - self._call_command('purge_old_personal_api_key_events', '-15') - - with self.assertRaises(CommandError): - self._call_command('purge_old_personal_api_key_events', '15.3') - - with self.assertRaises(CommandError): - self._call_command('purge_old_personal_api_key_events', '15', '15') - - with self.assertRaises(CommandError): - self._call_command('purge_old_personal_api_key_events', 'abc', '15') - - self.assertCountEqual(PersonApiKeyEvent.objects.all(), [event]) diff --git a/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py b/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py new file mode 100644 index 0000000000..98d5da75d6 --- /dev/null +++ b/ietf/person/migrations/0002_alter_historicalperson_ascii_and_more.py @@ -0,0 +1,82 @@ +# Generated by Django 4.2.13 on 2024-05-22 18:50 + +from django.db import migrations, models +import ietf.person.models + + +class Migration(migrations.Migration): + + dependencies = [ + ("person", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalperson", + name="ascii", + field=models.CharField( + help_text="Name as rendered in ASCII (Latin, unaccented) characters.", + max_length=255, + validators=[ietf.person.models.name_character_validator], + verbose_name="Full Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="historicalperson", + name="ascii_short", + field=models.CharField( + blank=True, + help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", + max_length=32, + null=True, + validators=[ietf.person.models.name_character_validator], + verbose_name="Abbreviated Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="historicalperson", + name="plain", + field=models.CharField( + blank=True, + default="", + help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", + max_length=64, + validators=[ietf.person.models.name_character_validator], + verbose_name="Plain Name correction (Unicode)", + ), + ), + migrations.AlterField( + model_name="person", + name="ascii", + field=models.CharField( + help_text="Name as rendered in ASCII (Latin, unaccented) characters.", + max_length=255, + validators=[ietf.person.models.name_character_validator], + verbose_name="Full Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="person", + name="ascii_short", + field=models.CharField( + blank=True, + help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", + max_length=32, + null=True, + validators=[ietf.person.models.name_character_validator], + verbose_name="Abbreviated Name (ASCII)", + ), + ), + migrations.AlterField( + model_name="person", + name="plain", + field=models.CharField( + blank=True, + default="", + help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", + max_length=64, + validators=[ietf.person.models.name_character_validator], + verbose_name="Plain Name correction (Unicode)", + ), + ), + ] diff --git a/ietf/person/models.py b/ietf/person/models.py index 0bb2b149e1..0c25152361 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -37,8 +37,12 @@ def name_character_validator(value): - if '/' in value: - raise ValidationError('Name cannot contain "/" character.') + disallowed = "@:/" + found = set(disallowed).intersection(value) + if len(found) > 0: + raise ValidationError( + f"This name cannot contain the characters {', '.join(disallowed)}" + ) class Person(models.Model): @@ -48,11 +52,11 @@ class Person(models.Model): # The normal unicode form of the name. This must be # set to the same value as the ascii-form if equal. name = models.CharField("Full Name (Unicode)", max_length=255, db_index=True, help_text="Preferred long form of name.", validators=[name_character_validator]) - plain = models.CharField("Plain Name correction (Unicode)", max_length=64, default='', blank=True, help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.") + plain = models.CharField("Plain Name correction (Unicode)", max_length=64, default='', blank=True, help_text="Use this if you have a Spanish double surname. Don't use this for nicknames, and don't use it unless you've actually observed that the datatracker shows your name incorrectly.", validators=[name_character_validator]) # The normal ascii-form of the name. - ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.") + ascii = models.CharField("Full Name (ASCII)", max_length=255, help_text="Name as rendered in ASCII (Latin, unaccented) characters.", validators=[name_character_validator]) # The short ascii-form of the name. Also in alias table if non-null - ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).") + ascii_short = models.CharField("Abbreviated Name (ASCII)", max_length=32, null=True, blank=True, help_text="Example: A. Nonymous. Fill in this with initials and surname only if taking the initials and surname of the ASCII name above produces an incorrect initials-only form. (Blank is OK).", validators=[name_character_validator]) pronouns_selectable = jsonfield.JSONCharField("Pronouns", max_length=120, blank=True, null=True, default=list ) pronouns_freetext = models.CharField(" ", max_length=30, null=True, blank=True, help_text="Optionally provide your personal pronouns. These will be displayed on your public profile page and alongside your name in Meetecho and, in future, other systems. Select any number of the checkboxes OR provide a custom string up to 30 characters.") biography = models.TextField(blank=True, help_text="Short biography for use on leadership pages. Use plain text or reStructuredText markup.") diff --git a/ietf/person/tasks.py b/ietf/person/tasks.py new file mode 100644 index 0000000000..f0c979fa26 --- /dev/null +++ b/ietf/person/tasks.py @@ -0,0 +1,59 @@ +# Copyright The IETF Trust 2024, All Rights Reserved +# +# Celery task definitions +# +import datetime + +from celery import shared_task + +from django.conf import settings +from django.utils import timezone + +from ietf.utils import log +from ietf.utils.mail import send_mail +from .models import PersonalApiKey, PersonApiKeyEvent + + +@shared_task +def send_apikey_usage_emails_task(days): + """Send usage emails to Persons who have API keys""" + earliest = timezone.now() - datetime.timedelta(days=days) + keys = PersonalApiKey.objects.filter( + valid=True, + personapikeyevent__time__gt=earliest, + ).distinct() + for key in keys: + events = PersonApiKeyEvent.objects.filter(key=key, time__gt=earliest) + count = events.count() + events = events[:32] + if count: + key_name = key.hash()[:8] + subject = "API key usage for key '%s' for the last %s days" % ( + key_name, + days, + ) + to = key.person.email_address() + frm = settings.DEFAULT_FROM_EMAIL + send_mail( + None, + to, + frm, + subject, + "utils/apikey_usage_report.txt", + { + "person": key.person, + "days": days, + "key": key, + "key_name": key_name, + "count": count, + "events": events, + }, + ) + +@shared_task +def purge_personal_api_key_events_task(keep_days): + keep_since = timezone.now() - datetime.timedelta(days=keep_days) + old_events = PersonApiKeyEvent.objects.filter(time__lt=keep_since) + count = len(old_events) + old_events.delete() + log.log(f"Deleted {count} PersonApiKeyEvents older than {keep_since}") diff --git a/ietf/person/tests.py b/ietf/person/tests.py index be3cfc0562..9da201b707 100644 --- a/ietf/person/tests.py +++ b/ietf/person/tests.py @@ -4,6 +4,7 @@ import datetime import json +import mock from io import StringIO, BytesIO from PIL import Image @@ -25,8 +26,9 @@ from ietf.nomcom.models import NomCom from ietf.nomcom.test_data import nomcom_test_data from ietf.nomcom.factories import NomComFactory, NomineeFactory, NominationFactory, FeedbackFactory, PositionFactory -from ietf.person.factories import EmailFactory, PersonFactory -from ietf.person.models import Person, Alias +from ietf.person.factories import EmailFactory, PersonFactory, PersonApiKeyEventFactory +from ietf.person.models import Person, Alias, PersonApiKeyEvent +from ietf.person.tasks import purge_personal_api_key_events_task from ietf.person.utils import (merge_persons, determine_merge_order, send_merge_notification, handle_users, get_extra_primary, dedupe_aliases, move_related_objects, merge_nominees, handle_reviewer_settings, get_dots) @@ -246,9 +248,11 @@ def test_cdn_photo_url_cdn_off(self): self.assertNotIn('cdn-cgi/photo',p.cdn_photo_url()) def test_invalid_name_characters_rejected(self): - slash_person = PersonFactory.build(name='I have a /', user=None) # build() does not save the new object - with self.assertRaises(ValidationError): - slash_person.full_clean() # calls validators (save() does *not*) + for disallowed in "/:@": + # build() does not save the new object + person_with_bad_name = PersonFactory.build(name=f"I have a {disallowed}", user=None) + with self.assertRaises(ValidationError, msg=f"Name with a {disallowed} char should be rejected"): + person_with_bad_name.full_clean() # calls validators (save() does *not*) class PersonUtilsTests(TestCase): @@ -448,3 +452,16 @@ def test_dots(self): self.assertEqual(get_dots(ncmember),['nomcom']) ncchair = RoleFactory(group__acronym='nomcom2020',group__type_id='nomcom',name_id='chair').person self.assertEqual(get_dots(ncchair),['nomcom']) + + +class TaskTests(TestCase): + @mock.patch("ietf.person.tasks.log.log") + def test_purge_personal_api_key_events_task(self, mock_log): + now = timezone.now() + old_event = PersonApiKeyEventFactory(time=now - datetime.timedelta(days=1, minutes=1)) + young_event = PersonApiKeyEventFactory(time=now - datetime.timedelta(days=1, minutes=-1)) + purge_personal_api_key_events_task(keep_days=1) + self.assertFalse(PersonApiKeyEvent.objects.filter(pk=old_event.pk).exists()) + self.assertTrue(PersonApiKeyEvent.objects.filter(pk=young_event.pk).exists()) + self.assertTrue(mock_log.called) + self.assertIn("Deleted 1", mock_log.call_args[0][0]) diff --git a/ietf/person/utils.py b/ietf/person/utils.py index eb2742ed30..5ed90591f9 100755 --- a/ietf/person/utils.py +++ b/ietf/person/utils.py @@ -3,10 +3,8 @@ import datetime -import os import pprint import sys -import syslog from django.contrib import admin from django.core.cache import cache @@ -17,14 +15,14 @@ import debug # pyflakes:ignore from ietf.person.models import Person, Alias, Email +from ietf.utils import log from ietf.utils.mail import send_mail def merge_persons(request, source, target, file=sys.stdout, verbose=False): changes = [] # write log - syslog.openlog(str(os.path.basename(__file__)), syslog.LOG_PID, syslog.LOG_USER) - syslog.syslog("Merging person records {} => {}".format(source.pk,target.pk)) + log.log(f"Merging person records {source.pk} => {target.pk}") # handle primary emails for email in get_extra_primary(source,target): @@ -118,7 +116,7 @@ def handle_users(source,target,check_only=False): if source.user and target.user: message = "DATATRACKER LOGIN ACTION: retaining login: {}, removing login: {}".format(target.user,source.user) if not check_only: - syslog.syslog('merge-person-records: deactivating user {}'.format(source.user.username)) + log.log(f"merge-person-records: deactivating user {source.user.username}") user = source.user source.user = None source.save() diff --git a/ietf/review/policies.py b/ietf/review/policies.py index 6738db95ff..91398a1b24 100644 --- a/ietf/review/policies.py +++ b/ietf/review/policies.py @@ -131,12 +131,15 @@ def _update_skip_next(self, rotation_pks, assignee_person): assignee_index = rotation_pks.index(assignee_person.pk) skipped = rotation_pks[0:assignee_index] skipped_settings = self.team.reviewersettings_set.filter(person__in=skipped) # list of PKs is valid here + changed = [] for ss in skipped_settings: - ss.skip_next = max(0, ss.skip_next - 1) # ensure we don't go negative - bulk_update_with_history(skipped_settings, + if ss.skip_next > 0: + ss.skip_next = max(0, ss.skip_next - 1) # ensure we don't go negative + ss._change_reason = "Skip count decremented" + changed.append(ss) + bulk_update_with_history(changed, ReviewerSettings, - ['skip_next'], - default_change_reason='skipped') + ['skip_next']) def _assignment_in_order(self, rotation_pks, assignee_person): """Is this an in-order assignment?""" @@ -262,12 +265,15 @@ def _filter_unavailable_reviewers(self, reviewers, review_req=None): def _clear_request_next_assignment(self, person): s = self._reviewer_settings_for(person) - s.request_assignment_next = False - s.save() + if s.request_assignment_next: + s.request_assignment_next = False + s._change_reason = "Clearing request next assignment" + s.save() def _add_skip(self, person): s = self._reviewer_settings_for(person) s.skip_next += 1 + s._change_reason = "Incrementing skip count" s.save() def _reviewer_settings_for(self, person): @@ -484,6 +490,7 @@ def set_wants_to_be_next(self, reviewer_person): # Instead, the "assign me next" flag is set. settings = self._reviewer_settings_for(reviewer_person) settings.request_assignment_next = True + settings._change_reason = "Setting request next assignment" settings.save() def _update_skip_next(self, rotation_pks, assignee_person): @@ -523,20 +530,22 @@ def _update_skip_next(self, rotation_pks, assignee_person): min_skip_next = min([rs.skip_next for rs in rotation_settings.values()]) next_reviewer_index = None + changed = [] for index, pk in enumerate(unfolded_rotation_pks): rs = rotation_settings.get(pk) if (rs is None) or (rs.skip_next == min_skip_next): next_reviewer_index = index break else: - rs.skip_next = max(0, rs.skip_next - 1) # ensure never negative + if rs.skip_next > 0: + rs.skip_next = max(0, rs.skip_next - 1) # ensure never negative + rs._change_reason = "Skip count decremented" + changed.append(rs) log.assertion('next_reviewer_index is not None') # some entry in the list must have the minimum value - - bulk_update_with_history(rotation_settings.values(), + bulk_update_with_history(changed, ReviewerSettings, - ['skip_next'], - default_change_reason='skipped') + ['skip_next']) next_reviewer_pk = unfolded_rotation_pks[next_reviewer_index] NextReviewerInTeam.objects.update_or_create( @@ -578,6 +587,7 @@ def set_wants_to_be_next(self, reviewer_person): # who rejected a review and no further action is needed. settings = self._reviewer_settings_for(reviewer_person) settings.request_assignment_next = True + settings._change_reason = "Setting request next assignment" settings.save() diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index 0e51ff8ca2..a241d2b5eb 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -3,14 +3,11 @@ import datetime -import os -import shutil from pyquery import PyQuery import debug # pyflakes:ignore -from django.conf import settings from django.urls import reverse from django.utils import timezone @@ -27,24 +24,6 @@ class SecrMeetingTestCase(TestCase): settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH'] - def setUp(self): - super().setUp() - self.bluesheet_dir = self.tempdir('bluesheet') - self.bluesheet_path = os.path.join(self.bluesheet_dir,'blue_sheet.rtf') - self.saved_secr_blue_sheet_path = settings.SECR_BLUE_SHEET_PATH - settings.SECR_BLUE_SHEET_PATH = self.bluesheet_path - - # n.b., the bluesheet upload relies on SECR_PROCEEDINGS_DIR being the same - # as AGENDA_PATH. This is probably a bug, but may not be worth fixing if - # the secr app is on the way out. - self.saved_secr_proceedings_dir = settings.SECR_PROCEEDINGS_DIR - settings.SECR_PROCEEDINGS_DIR = settings.AGENDA_PATH - - def tearDown(self): - settings.SECR_PROCEEDINGS_DIR = self.saved_secr_proceedings_dir - settings.SECR_BLUE_SHEET_PATH = self.saved_secr_blue_sheet_path - shutil.rmtree(self.bluesheet_dir) - super().tearDown() def test_main(self): "Main Test" @@ -416,4 +395,4 @@ def test_get_times(self): times = get_times(meeting,day) values = [ x[0] for x in times ] self.assertTrue(times) - self.assertTrue(timeslot.time.strftime('%H%M') in values) \ No newline at end of file + self.assertTrue(timeslot.time.strftime('%H%M') in values) diff --git a/ietf/secr/sreq/forms.py b/ietf/secr/sreq/forms.py index 1100bc7c8a..4a0f449b2a 100644 --- a/ietf/secr/sreq/forms.py +++ b/ietf/secr/sreq/forms.py @@ -13,6 +13,7 @@ from ietf.meeting.models import ResourceAssociation, Constraint from ietf.person.fields import SearchablePersonsField from ietf.person.models import Person +from ietf.utils.fields import ModelMultipleChoiceField from ietf.utils.html import clean_text_field from ietf.utils import log @@ -57,7 +58,7 @@ def __init__(self,*args,**kwargs): self.fields['group'].widget.choices = choices -class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField): +class NameModelMultipleChoiceField(ModelMultipleChoiceField): def label_from_instance(self, name): return name.desc @@ -159,7 +160,7 @@ def __init__(self, group, meeting, data=None, *args, **kwargs): self.fields['resources'].widget = forms.MultipleHiddenInput() self.fields['timeranges'].widget = forms.MultipleHiddenInput() # and entirely replace bethere - no need to support searching if input is hidden - self.fields['bethere'] = forms.ModelMultipleChoiceField( + self.fields['bethere'] = ModelMultipleChoiceField( widget=forms.MultipleHiddenInput, required=False, queryset=Person.objects.all(), ) diff --git a/ietf/secr/telechat/urls.py b/ietf/secr/telechat/urls.py index 0f2ff4aace..08c51eab5f 100644 --- a/ietf/secr/telechat/urls.py +++ b/ietf/secr/telechat/urls.py @@ -11,5 +11,4 @@ url(r'^(?P[0-9\-]+)/management/$', views.management), url(r'^(?P[0-9\-]+)/minutes/$', views.minutes), url(r'^(?P[0-9\-]+)/roll-call/$', views.roll_call), - url(r'^new/$', views.new), ] diff --git a/ietf/secr/telechat/views.py b/ietf/secr/telechat/views.py index 356a9b9349..c39aecf748 100644 --- a/ietf/secr/telechat/views.py +++ b/ietf/secr/telechat/views.py @@ -17,7 +17,7 @@ from ietf.person.models import Person from ietf.doc.lastcall import request_last_call from ietf.doc.mails import email_state_changed -from ietf.iesg.models import TelechatDate, TelechatAgendaItem, Telechat +from ietf.iesg.models import TelechatDate, TelechatAgendaItem from ietf.iesg.agenda import agenda_data, get_doc_section from ietf.ietfauth.utils import role_required from ietf.secr.telechat.forms import BallotForm, ChangeStateForm, DateSelectForm, TELECHAT_TAGS @@ -419,18 +419,6 @@ def minutes(request, date): 'da_docs': da_docs}, ) -@role_required('Secretariat') -def new(request): - ''' - This view creates a new telechat agenda and redirects to the default view - ''' - if request.method == 'POST': - date = request.POST['date'] - # create legacy telechat record - Telechat.objects.create(telechat_date=date) - - messages.success(request,'New Telechat Agenda created') - return redirect('ietf.secr.telechat.views.doc', date=date) @role_required('Secretariat') def roll_call(request, date): diff --git a/ietf/secr/templates/telechat/group.html b/ietf/secr/templates/telechat/group.html index 890c451e83..4e04f0e16e 100644 --- a/ietf/secr/templates/telechat/group.html +++ b/ietf/secr/templates/telechat/group.html @@ -3,7 +3,7 @@ Does anyone have an objection to the creation of this working group being sent for EXTERNAL REVIEW?

External Review APPROVED; "The Secretariat will send a Working Group Review announcement with a copy to new-work and place it back on the agenda for the next telechat."

External Review NOT APPROVED; -
+
The Secretariat will wait for instructions from
The IESG decides the document needs more time in INTERNAL REVIEW. The Secretariat will put it back on the agenda for the next teleconference in the same category.
The IESG has made changes since the charter was seen in INTERNAL REVIEW, and decides to send it back to INTERNAL REVIEW the charter again. diff --git a/ietf/settings.py b/ietf/settings.py index dca3fb132a..13b7506673 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -26,11 +26,6 @@ warnings.filterwarnings("ignore", message="Report.file_reporters will no longer be available in Coverage.py 4.2", module="coverage.report") warnings.filterwarnings("ignore", message="Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated", module="bleach") warnings.filterwarnings("ignore", message="HTTPResponse.getheader\\(\\) is deprecated", module='selenium.webdriver') -try: - import syslog - syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER) -except ImportError: - pass BASE_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.abspath(BASE_DIR + "/..")) @@ -125,6 +120,10 @@ # In the future (relative to 4.2), the default will become 'django.db.models.BigAutoField.' DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +# OIDC configuration +_SITE_URL = os.environ.get("OIDC_SITE_URL", None) +if _SITE_URL is not None: + SITE_URL = _SITE_URL if SERVER_MODE == 'production': MEDIA_ROOT = '/a/www/www6s/lib/dt/media/' @@ -236,11 +235,11 @@ def skip_unreadable_post(record): # 'loggers': { 'django': { - 'handlers': ['debug_console', 'mail_admins'], + 'handlers': ['console', 'mail_admins'], 'level': 'INFO', }, 'django.request': { - 'handlers': ['debug_console'], + 'handlers': ['console'], 'level': 'ERROR', }, 'django.server': { @@ -248,13 +247,21 @@ def skip_unreadable_post(record): 'level': 'INFO', }, 'django.security': { - 'handlers': ['debug_console', ], + 'handlers': ['console', ], + 'level': 'INFO', + }, + 'oidc_provider': { + 'handlers': ['console', ], + 'level': 'DEBUG', + }, + 'datatracker': { + 'handlers': ['console'], + 'level': 'INFO', + }, + 'celery': { + 'handlers': ['console'], 'level': 'INFO', }, - 'oidc_provider': { - 'handlers': ['debug_console', ], - 'level': 'DEBUG', - }, }, # # No logger filters @@ -265,13 +272,6 @@ def skip_unreadable_post(record): 'class': 'logging.StreamHandler', 'formatter': 'plain', }, - 'syslog': { - 'level': 'DEBUG', - 'class': 'logging.handlers.SysLogHandler', - 'facility': 'user', - 'formatter': 'plain', - 'address': '/dev/log', - }, 'debug_console': { # Active only when DEBUG=True 'level': 'DEBUG', @@ -325,18 +325,14 @@ def skip_unreadable_post(record): 'style': '{', 'format': '{levelname}: {name}:{lineno}: {message}', }, + 'json' : { + "class": "ietf.utils.jsonlogger.DatatrackerJsonFormatter", + "style": "{", + "format": "{asctime}{levelname}{message}{name}{pathname}{lineno}{funcName}{process}", + } }, } -# This should be overridden by settings_local for any logger where debug (or -# other) custom log settings are wanted. Use "ietf/manage.py showloggers -l" -# to show registered loggers. The content here should match the levels above -# and is shown as an example: -UTILS_LOGGER_LEVELS: Dict[str, str] = { -# 'django': 'INFO', -# 'django.server': 'INFO', -} - # End logging # ------------------------------------------------------------------------ @@ -671,6 +667,8 @@ def skip_unreadable_post(record): INTERNET_DRAFT_PDF_PATH = '/a/www/ietf-datatracker/pdf/' RFC_PATH = '/a/www/ietf-ftp/rfc/' CHARTER_PATH = '/a/ietfdata/doc/charter/' +CHARTER_COPY_PATH = '/a/www/ietf-ftp/ietf' # copy 1wg-charters files here if set +GROUP_SUMMARY_PATH = '/a/www/ietf-ftp/ietf' BOFREQ_PATH = '/a/ietfdata/doc/bofreq/' CONFLICT_REVIEW_PATH = '/a/ietfdata/doc/conflict-review' STATUS_CHANGE_PATH = '/a/ietfdata/doc/status-change' @@ -679,16 +677,18 @@ def skip_unreadable_post(record): IPR_DOCUMENT_PATH = '/a/www/ietf-ftp/ietf/IPR/' # Move drafts to this directory when they expire INTERNET_DRAFT_ARCHIVE_DIR = '/a/ietfdata/doc/draft/collection/draft-archive/' -# The following directory contains linked copies of all drafts, but don't -# write anything to this directory -- its content is maintained by ghostlinkd: +# The following directory contains copies of all drafts - it used to be +# a set of hardlinks maintained by ghostlinkd, but is now explicitly written to INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/a/ietfdata/doc/draft/archive' MEETING_RECORDINGS_DIR = '/a/www/audio' DERIVED_DIR = '/a/ietfdata/derived' +FTP_DIR = '/a/ftp' +ALL_ID_DOWNLOAD_DIR = '/a/www/www6s/download' DOCUMENT_FORMAT_ALLOWLIST = ["txt", "ps", "pdf", "xml", "html", ] # Mailing list info URL for lists hosted on the IETF servers -MAILING_LIST_INFO_URL = "https://www.ietf.org/mailman/listinfo/%(list_addr)s" +MAILING_LIST_INFO_URL = "https://mailman3.%(domain)s/mailman3/lists/%(list_addr)s.%(domain)s" MAILING_LIST_ARCHIVE_URL = "https://mailarchive.ietf.org" # Liaison Statement Tool settings (one is used in DOC_HREFS below) @@ -815,7 +815,8 @@ def skip_unreadable_post(record): # Max time to allow for validation before a submission is subject to cancellation IDSUBMIT_MAX_VALIDATION_TIME = datetime.timedelta(minutes=20) -IDSUBMIT_MANUAL_STAGING_DIR = '/tmp/' +# Age at which a submission expires if not posted +IDSUBMIT_EXPIRATION_AGE = datetime.timedelta(days=14) IDSUBMIT_FILE_TYPES = ( 'txt', @@ -975,8 +976,6 @@ def skip_unreadable_post(record): # Account settings DAYS_TO_EXPIRE_REGISTRATION_LINK = 3 MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK = 60 -HTPASSWD_COMMAND = "/usr/bin/htpasswd" -HTPASSWD_FILE = "/www/htpasswd" # Generation of pdf files GHOSTSCRIPT_COMMAND = "/usr/bin/gs" @@ -987,12 +986,11 @@ def skip_unreadable_post(record): # Timezone files for iCalendar TZDATA_ICS_PATH = BASE_DIR + '/../vzic/zoneinfo/' -SECR_BLUE_SHEET_PATH = '/a/www/ietf-datatracker/documents/blue_sheet.rtf' -SECR_BLUE_SHEET_URL = IDTRACKER_BASE_URL + '/documents/blue_sheet.rtf' -SECR_INTERIM_LISTING_DIR = '/a/www/www6/meeting/interim' -SECR_MAX_UPLOAD_SIZE = 40960000 -SECR_PROCEEDINGS_DIR = '/a/www/www6s/proceedings/' -SECR_PPT2PDF_COMMAND = ['/usr/bin/soffice','--headless','--convert-to','pdf:writer_globaldocument_pdf_Export','--outdir'] +DATATRACKER_MAX_UPLOAD_SIZE = 40960000 +PPT2PDF_COMMAND = [ + "/usr/bin/soffice", "--headless", "--convert-to", "pdf:writer_globaldocument_pdf_Export", "--outdir" +] + STATS_REGISTRATION_ATTENDEES_JSON_URL = 'https://registration.ietf.org/{number}/attendees/' PROCEEDINGS_VERSION_CHANGES = [ 0, # version 1 @@ -1060,14 +1058,6 @@ def skip_unreadable_post(record): TEST_DATA_DIR = os.path.abspath(BASE_DIR + "/../test/data") -# Path to the email alias lists. Used by ietf.utils.aliases -DRAFT_ALIASES_PATH = os.path.join(TEST_DATA_DIR, "draft-aliases") -DRAFT_VIRTUAL_PATH = os.path.join(TEST_DATA_DIR, "draft-virtual") -DRAFT_VIRTUAL_DOMAIN = "virtual.ietf.org" - -GROUP_ALIASES_PATH = os.path.join(TEST_DATA_DIR, "group-aliases") -GROUP_VIRTUAL_PATH = os.path.join(TEST_DATA_DIR, "group-virtual") -GROUP_VIRTUAL_DOMAIN = "virtual.ietf.org" POSTCONFIRM_PATH = "/a/postconfirm/wrapper" @@ -1204,81 +1194,83 @@ def skip_unreadable_post(record): MIDDLEWARE += DEV_MIDDLEWARE TEMPLATES[0]['OPTIONS']['context_processors'] += DEV_TEMPLATE_CONTEXT_PROCESSORS -if 'CACHES' not in locals(): - if SERVER_MODE == 'production': +if "CACHES" not in locals(): + if SERVER_MODE == "production": + MEMCACHED_HOST = os.environ.get("MEMCACHED_SERVICE_HOST", "127.0.0.1") + MEMCACHED_PORT = os.environ.get("MEMCACHED_SERVICE_PORT", "11211") CACHES = { - 'default': { - 'BACKEND': 'ietf.utils.cache.LenientMemcacheCache', - 'LOCATION': '127.0.0.1:11211', - 'VERSION': __version__, - 'KEY_PREFIX': 'ietf:dt', - 'KEY_FUNCTION': lambda key, key_prefix, version: ( + "default": { + "BACKEND": "ietf.utils.cache.LenientMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", + "VERSION": __version__, + "KEY_PREFIX": "ietf:dt", + "KEY_FUNCTION": lambda key, key_prefix, version: ( f"{key_prefix}:{version}:{sha384(str(key).encode('utf8')).hexdigest()}" ), }, - 'sessions': { - 'BACKEND': 'ietf.utils.cache.LenientMemcacheCache', - 'LOCATION': '127.0.0.1:11211', + "sessions": { + "BACKEND": "ietf.utils.cache.LenientMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", # No release-specific VERSION setting. - 'KEY_PREFIX': 'ietf:dt', + "KEY_PREFIX": "ietf:dt", }, - 'htmlized': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/a/cache/datatracker/htmlized', - 'OPTIONS': { - 'MAX_ENTRIES': 100000, # 100,000 + "htmlized": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/htmlized", + "OPTIONS": { + "MAX_ENTRIES": 100000, # 100,000 }, }, - 'pdfized': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/a/cache/datatracker/pdfized', - 'OPTIONS': { - 'MAX_ENTRIES': 100000, # 100,000 + "pdfized": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/pdfized", + "OPTIONS": { + "MAX_ENTRIES": 100000, # 100,000 }, }, - 'slowpages': { - 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/a/cache/datatracker/slowpages', - 'OPTIONS': { - 'MAX_ENTRIES': 5000, + "slowpages": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/slowpages", + "OPTIONS": { + "MAX_ENTRIES": 5000, }, }, } else: CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", #'BACKEND': 'ietf.utils.cache.LenientMemcacheCache', #'LOCATION': '127.0.0.1:11211', #'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'VERSION': __version__, - 'KEY_PREFIX': 'ietf:dt', + "VERSION": __version__, + "KEY_PREFIX": "ietf:dt", }, - 'sessions': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + "sessions": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", }, - 'htmlized': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + "htmlized": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", #'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/var/cache/datatracker/htmlized', - 'OPTIONS': { - 'MAX_ENTRIES': 1000, + "LOCATION": "/var/cache/datatracker/htmlized", + "OPTIONS": { + "MAX_ENTRIES": 1000, }, }, - 'pdfized': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + "pdfized": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", #'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/var/cache/datatracker/pdfized', - 'OPTIONS': { - 'MAX_ENTRIES': 1000, + "LOCATION": "/var/cache/datatracker/pdfized", + "OPTIONS": { + "MAX_ENTRIES": 1000, }, }, - 'slowpages': { - 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', + "slowpages": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", #'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', - 'LOCATION': '/var/cache/datatracker/', - 'OPTIONS': { - 'MAX_ENTRIES': 5000, + "LOCATION": "/var/cache/datatracker/", + "OPTIONS": { + "MAX_ENTRIES": 5000, }, }, } diff --git a/ietf/settings_test.py b/ietf/settings_test.py index 3f69f0ae38..024512a8db 100755 --- a/ietf/settings_test.py +++ b/ietf/settings_test.py @@ -60,3 +60,36 @@ def __getitem__(self, item): TEMPLATES[0]['OPTIONS']['context_processors'] = [ p for p in TEMPLATES[0]['OPTIONS']['context_processors'] if not p in DEV_TEMPLATE_CONTEXT_PROCESSORS ] # pyflakes:ignore REQUEST_PROFILE_STORE_ANONYMOUS_SESSIONS = False + +# Override loggers with a safer set in case things go to the log during testing. Specifically, +# make sure there are no syslog loggers that might send things to a real syslog. +LOGGING["loggers"] = { # pyflakes:ignore + 'django': { + 'handlers': ['debug_console'], + 'level': 'INFO', + }, + 'django.request': { + 'handlers': ['debug_console'], + 'level': 'ERROR', + }, + 'django.server': { + 'handlers': ['django.server'], + 'level': 'INFO', + }, + 'django.security': { + 'handlers': ['debug_console', ], + 'level': 'INFO', + }, + 'oidc_provider': { + 'handlers': ['debug_console', ], + 'level': 'DEBUG', + }, + 'datatracker': { + 'handlers': ['debug_console'], + 'level': 'INFO', + }, + 'celery': { + 'handlers': ['debug_console'], + 'level': 'INFO', + }, +} diff --git a/ietf/static/css/ietf.scss b/ietf/static/css/ietf.scss index ca960c0378..062358c0e2 100644 --- a/ietf/static/css/ietf.scss +++ b/ietf/static/css/ietf.scss @@ -1183,3 +1183,8 @@ td.position-empty { } } } + +blockquote { + padding-left: 1rem; + border-left: solid 1px var(--bs-body-color); +} diff --git a/ietf/static/js/ietf.js b/ietf/static/js/ietf.js index 215d80553c..74fd39a85f 100644 --- a/ietf/static/js/ietf.js +++ b/ietf/static/js/ietf.js @@ -57,7 +57,7 @@ $(document) var text = $(this) .text(); // insert some at strategic places - var newtext = text.replace(/([@._+])/g, "$1"); + var newtext = text.replace(/(\S)([@._+])(\S)/g, "$1$2$3"); if (newtext === text) { return; } diff --git a/ietf/stats/management/commands/fetch_meeting_attendance.py b/ietf/stats/management/commands/fetch_meeting_attendance.py index 82db6570ee..e17ae567fa 100644 --- a/ietf/stats/management/commands/fetch_meeting_attendance.py +++ b/ietf/stats/management/commands/fetch_meeting_attendance.py @@ -1,8 +1,6 @@ # Copyright The IETF Trust 2017-2019, All Rights Reserved # Copyright 2016 IETF Trust -import syslog - from django.core.management.base import BaseCommand, CommandError from django.utils import timezone @@ -10,10 +8,8 @@ from ietf.meeting.models import Meeting from ietf.stats.utils import fetch_attendance_from_meetings +from ietf.utils import log -logtag = __name__.split('.')[-1] -logname = "user.log" -syslog.openlog(str(logtag), syslog.LOG_PID, syslog.LOG_USER) class Command(BaseCommand): help = "Fetch meeting attendee figures from ietf.org/registration/attendees." @@ -43,4 +39,4 @@ def handle(self, *args, **options): if self.stdout.isatty(): self.stdout.write(msg+'\n') # make debugging a bit easier else: - syslog.syslog(msg) + log.log(msg) diff --git a/ietf/submit/checkers.py b/ietf/submit/checkers.py index 5822f155f5..d29e2a2355 100644 --- a/ietf/submit/checkers.py +++ b/ietf/submit/checkers.py @@ -14,8 +14,8 @@ import debug # pyflakes:ignore +from ietf.utils import tool_version from ietf.utils.log import log, assertion -from ietf.utils.models import VersionInfo from ietf.utils.pipe import pipe from ietf.utils.test_runner import set_coverage_checking @@ -177,8 +177,10 @@ def check_file_txt(self, path): model_list = list(set(model_list)) command = "xym" - cmd_version = VersionInfo.objects.get(command=command).version - message = "%s:\n%s\n\n" % (cmd_version, out.replace('\n\n','\n').strip() if code == 0 else err) + message = "{version}:\n{output}\n\n".format( + version=tool_version[command], + output=out.replace('\n\n', '\n').strip() if code == 0 else err, + ) results.append({ "name": name, @@ -209,7 +211,6 @@ def check_file_txt(self, path): # pyang cmd_template = settings.SUBMIT_PYANG_COMMAND command = [ w for w in cmd_template.split() if not '=' in w ][0] - cmd_version = VersionInfo.objects.get(command=command).version cmd = cmd_template.format(libs=modpath, model=path) venv_path = os.environ.get('VIRTUAL_ENV') or os.path.join(os.getcwd(), 'env') venv_bin = os.path.join(venv_path, 'bin') @@ -238,14 +239,17 @@ def check_file_txt(self, path): except ValueError: pass #passed = passed and code == 0 # For the submission tool. Yang checks always pass - message += "%s: %s:\n%s\n" % (cmd_version, cmd_template, out+"No validation errors\n" if (code == 0 and len(err) == 0) else out+err) + message += "{version}: {template}:\n{output}\n".format( + version=tool_version[command], + template=cmd_template, + output=out + "No validation errors\n" if (code == 0 and len(err) == 0) else out + err, + ) # yanglint set_coverage_checking(False) # we can't count the following as it may or may not be run, depending on setup if settings.SUBMIT_YANGLINT_COMMAND and os.path.exists(settings.YANGLINT_BINARY): cmd_template = settings.SUBMIT_YANGLINT_COMMAND command = [ w for w in cmd_template.split() if not '=' in w ][0] - cmd_version = VersionInfo.objects.get(command=command).version cmd = cmd_template.format(model=path, rfclib=settings.SUBMIT_YANG_RFC_MODEL_DIR, tmplib=workdir, draftlib=settings.SUBMIT_YANG_DRAFT_MODEL_DIR, ianalib=settings.SUBMIT_YANG_IANA_MODEL_DIR, cataloglib=settings.SUBMIT_YANG_CATALOG_MODEL_DIR, ) @@ -264,7 +268,11 @@ def check_file_txt(self, path): except ValueError: pass #passed = passed and code == 0 # For the submission tool. Yang checks always pass - message += "%s: %s:\n%s\n" % (cmd_version, cmd_template, out+"No validation errors\n" if (code == 0 and len(err) == 0) else out+err) + message += "{version}: {template}:\n{output}\n".format( + version=tool_version[command], + template=cmd_template, + output=out + "No validation errors\n" if (code == 0 and len(err) == 0) else out + err, + ) set_coverage_checking(True) else: errors += 1 @@ -293,4 +301,4 @@ def check_file_txt(self, path): items = [ e for res in results for e in res["items"] ] info['items'] = items info['code']['yang'] = model_list - return passed, message, errors, warnings, info \ No newline at end of file + return passed, message, errors, warnings, info diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index f857ac9fd8..4e5644b36e 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -39,6 +39,7 @@ from ietf.submit.parsers.xml_parser import XMLParser from ietf.utils import log from ietf.utils.draft import PlaintextDraft +from ietf.utils.fields import ModelMultipleChoiceField from ietf.utils.text import normalize_text from ietf.utils.timezone import date_today from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError @@ -793,7 +794,7 @@ class EditSubmissionForm(forms.ModelForm): rev = forms.CharField(label='Revision', max_length=2, required=True) document_date = forms.DateField(required=True) pages = forms.IntegerField(required=True) - formal_languages = forms.ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False) + formal_languages = ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False) abstract = forms.CharField(widget=forms.Textarea, required=True, strip=False) note = forms.CharField(label=mark_safe('Comment to the Secretariat'), widget=forms.Textarea, required=False, strip=False) diff --git a/ietf/submit/mail.py b/ietf/submit/mail.py index 1953ad81c9..d7da7e21e0 100644 --- a/ietf/submit/mail.py +++ b/ietf/submit/mail.py @@ -1,33 +1,22 @@ # Copyright The IETF Trust 2013-2020, All Rights Reserved # -*- coding: utf-8 -*- - -import re -import email -import base64 import os -import pyzmail from django.conf import settings from django.urls import reverse as urlreverse -from django.core.exceptions import ValidationError from django.contrib.sites.models import Site from django.template.loader import render_to_string -from django.utils.encoding import force_str import debug # pyflakes:ignore -from ietf.utils.log import log from ietf.utils.mail import send_mail, send_mail_message from ietf.doc.models import Document -from ietf.ipr.mail import utc_from_string from ietf.person.models import Person -from ietf.message.models import Message, MessageAttachment +from ietf.message.models import Message from ietf.utils.accesstoken import generate_access_token -from ietf.mailtrigger.utils import gather_address_lists, get_base_submission_message_address -from ietf.submit.models import SubmissionEmailEvent, Submission +from ietf.mailtrigger.utils import gather_address_lists from ietf.submit.checkers import DraftIdnitsChecker -from ietf.utils.timezone import date_today def send_submission_confirmation(request, submission, chair_notice=False): @@ -196,181 +185,3 @@ def announce_to_authors(request, submission): 'group': group}, cc=cc) - -def get_reply_to(): - """Returns a new reply-to address for use with an outgoing message. This is an - address with "plus addressing" using a random string. Guaranteed to be unique""" - local,domain = get_base_submission_message_address().split('@') - while True: - rand = force_str(base64.urlsafe_b64encode(os.urandom(12))) - address = "{}+{}@{}".format(local,rand,domain) - q = Message.objects.filter(reply_to=address) - if not q: - return address - - -def process_response_email(msg): - """Saves an incoming message. msg=string. Message "To" field is expected to - be in the format ietf-submit+[identifier]@ietf.org. Expect to find a message with - a matching value in the reply_to field, associated to a submission. - Create a Message object for the incoming message and associate it to - the original message via new SubmissionEvent""" - message = email.message_from_string(force_str(msg)) - to = message.get('To') - - # exit if this isn't a response we're interested in (with plus addressing) - local,domain = get_base_submission_message_address().split('@') - if not re.match(r'^{}\+[a-zA-Z0-9_\-]{}@{}'.format(local,'{16}',domain),to): - return None - - try: - to_message = Message.objects.get(reply_to=to) - except Message.DoesNotExist: - log('Error finding matching message ({})'.format(to)) - return None - - try: - submission = to_message.manualevents.first().submission - except: - log('Error processing message ({})'.format(to)) - return None - - if not submission: - log('Error processing message - no submission ({})'.format(to)) - return None - - parts = pyzmail.parse.get_mail_parts(message) - body='' - for part in parts: - if part.is_body == 'text/plain' and part.disposition == None: - payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None) - body = body + payload + '\n' - - by = Person.objects.get(name="(System)") - msg = submit_message_from_message(message, body, by) - - desc = "Email: received message - manual post - {}-{}".format( - submission.name, - submission.rev) - - submission_email_event = SubmissionEmailEvent.objects.create( - submission = submission, - desc = desc, - msgtype = 'msgin', - by = by, - message = msg, - in_reply_to = to_message - ) - - save_submission_email_attachments(submission_email_event, parts) - - log("Received submission email from %s" % msg.frm) - return msg - - -def add_submission_email(request, remote_ip, name, rev, submission_pk, message, by, msgtype): - """Add email to submission history""" - - #in_reply_to = form.cleaned_data['in_reply_to'] - # create Message - parts = pyzmail.parse.get_mail_parts(message) - body='' - for part in parts: - if part.is_body == 'text/plain' and part.disposition == None: - payload, used_charset = pyzmail.decode_text(part.get_payload(), part.charset, None) - body = body + payload + '\n' - - msg = submit_message_from_message(message, body, by) - - if (submission_pk != None): - # Must exist - we're adding a message to an existing submission - submission = Submission.objects.get(pk=submission_pk) - else: - # Must not exist - submissions = Submission.objects.filter(name=name,rev=rev).exclude(state_id='cancel') - if submissions.count() > 0: - raise ValidationError("Submission {} already exists".format(name)) - - # create Submission using the name - try: - submission = Submission.objects.create( - state_id="waiting-for-draft", - remote_ip=remote_ip, - name=name, - rev=rev, - title=name, - note="", - submission_date=date_today(), - replaces="", - ) - from ietf.submit.utils import create_submission_event, docevent_from_submission - desc = "Submission created for rev {} in response to email".format(rev) - create_submission_event(request, - submission, - desc) - docevent_from_submission(submission, desc) - except Exception as e: - log("Exception: %s\n" % e) - raise - - if msgtype == 'msgin': - rs = "Received" - else: - rs = "Sent" - - desc = "{} message - manual post - {}-{}".format(rs, name, rev) - submission_email_event = SubmissionEmailEvent.objects.create( - desc = desc, - submission = submission, - msgtype = msgtype, - by = by, - message = msg) - #in_reply_to = in_reply_to - - save_submission_email_attachments(submission_email_event, parts) - return submission, submission_email_event - - -def submit_message_from_message(message,body,by=None): - """Returns a ietf.message.models.Message. msg=email.Message - A copy of mail.message_from_message with different body handling - """ - if not by: - by = Person.objects.get(name="(System)") - msg = Message.objects.create( - by = by, - subject = message.get('subject',''), - frm = message.get('from',''), - to = message.get('to',''), - cc = message.get('cc',''), - bcc = message.get('bcc',''), - reply_to = message.get('reply_to',''), - body = body, - time = utc_from_string(message.get('date', '')), - content_type = message.get('content_type', 'text/plain'), - ) - return msg - -def save_submission_email_attachments(submission_email_event, parts): - for part in parts: - if part.disposition != 'attachment': - continue - - if part.type == 'text/plain': - payload, used_charset = pyzmail.decode_text(part.get_payload(), - part.charset, - None) - encoding = "" - else: - # Need a better approach - for the moment we'll just handle these - # and encode as base64 - payload = base64.b64encode(part.get_payload()) - encoding = "base64" - - #name = submission_email_event.submission.name - - MessageAttachment.objects.create(message = submission_email_event.message, - content_type = part.type, - encoding = encoding, - filename=part.filename, - body=payload) diff --git a/ietf/submit/management/commands/manualpost_email.py b/ietf/submit/management/commands/manualpost_email.py deleted file mode 100644 index aaabfb39b7..0000000000 --- a/ietf/submit/management/commands/manualpost_email.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import io -import sys - -from django.core.management.base import BaseCommand, CommandError - -from ietf.submit.mail import process_response_email - -import debug # pyflakes:ignore - -class Command(BaseCommand): - help = ("Process incoming manual post email responses") - - def add_arguments(self, parser): - parser.add_argument('--email-file', dest='email', help='File containing email (default: stdin)') - - def handle(self, *args, **options): - email = options.get('email', None) - msg = None - - if not email: - msg = sys.stdin.read() - else: - msg = io.open(email, "r").read() - - try: - process_response_email(msg) - except ValueError as e: - raise CommandError(e) diff --git a/ietf/submit/tasks.py b/ietf/submit/tasks.py index 382bff7fae..9e279fa9f0 100644 --- a/ietf/submit/tasks.py +++ b/ietf/submit/tasks.py @@ -10,7 +10,8 @@ from ietf.submit.models import Submission from ietf.submit.utils import (cancel_submission, create_submission_event, process_uploaded_submission, - process_and_accept_uploaded_submission) + process_and_accept_uploaded_submission, run_all_yang_model_checks, + populate_yang_model_dirs) from ietf.utils import log @@ -37,20 +38,41 @@ def process_and_accept_uploaded_submission_task(submission_id): @shared_task def cancel_stale_submissions(): now = timezone.now() - stale_submissions = Submission.objects.filter( + # first check for submissions gone stale awaiting validation + stale_unvalidated_submissions = Submission.objects.filter( state_id='validating', ).annotate( submitted_at=Min('submissionevent__time'), ).filter( submitted_at__lt=now - settings.IDSUBMIT_MAX_VALIDATION_TIME, ) - for subm in stale_submissions: + for subm in stale_unvalidated_submissions: age = now - subm.submitted_at log.log(f'Canceling stale submission (id={subm.id}, age={age})') cancel_submission(subm) create_submission_event(None, subm, 'Submission canceled: validation checks took too long') + # now check for expired submissions + expired_submissions = Submission.objects.exclude( + state_id__in=["posted", "cancel"], + ).annotate( + submitted_at=Min("submissionevent__time"), + ).filter( + submitted_at__lt=now - settings.IDSUBMIT_EXPIRATION_AGE, + ) + for subm in expired_submissions: + age = now - subm.submitted_at + log.log(f'Canceling expired submission (id={subm.id}, age={age})') + cancel_submission(subm) + create_submission_event(None, subm, 'Submission canceled: expired without being posted') + + +@shared_task +def run_yang_model_checks_task(): + populate_yang_model_dirs() + run_all_yang_model_checks() + @shared_task(bind=True) def poke(self): log.log(f'Poked {self.name}, request id {self.request.id}') diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 1e432cb8ef..b48168f8a6 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -42,18 +42,17 @@ from ietf.group.utils import setup_default_community_list_for_group from ietf.meeting.models import Meeting from ietf.meeting.factories import MeetingFactory -from ietf.message.models import Message -from ietf.name.models import FormalLanguageName +from ietf.name.models import DraftSubmissionStateName, FormalLanguageName from ietf.person.models import Person from ietf.person.factories import UserFactory, PersonFactory, EmailFactory from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm from ietf.submit.models import Submission, Preapproval, SubmissionExtResource -from ietf.submit.mail import add_submission_email, process_response_email from ietf.submit.tasks import cancel_stale_submissions, process_and_accept_uploaded_submission_task +from ietf.submit.utils import apply_yang_checker_to_draft, run_all_yang_model_checks +from ietf.utils import tool_version from ietf.utils.accesstoken import generate_access_token -from ietf.utils.mail import outbox, empty_outbox, get_payload_text -from ietf.utils.models import VersionInfo +from ietf.utils.mail import outbox, get_payload_text from ietf.utils.test_utils import login_testing_unauthorized, TestCase from ietf.utils.timezone import date_today from ietf.utils.draft import PlaintextDraft @@ -198,9 +197,32 @@ def create_draft_submission_with_rev_mismatch(rev='01'): return draft, sub +class ManualSubmissionTests(TestCase): + def test_manualpost_view(self): + submission = SubmissionFactory(state_id="manual") + url = urlreverse("ietf.submit.views.manualpost") + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertIn( + urlreverse( + "ietf.submit.views.submission_status", + kwargs=dict(submission_id=submission.pk) + ), + q("#manual.submissions td a").attr("href") + ) + self.assertIn( + submission.name, + q("#manual.submissions td a").text() + ) + + def test_manualpost_cancel(self): + pass + class SubmitTests(BaseSubmitTestCase): def setUp(self): super().setUp() + (Path(settings.FTP_DIR) / "internet-drafts").mkdir() # Submit views assume there is a "next" IETF to look for cutoff dates against MeetingFactory(type_id='ietf', date=date_today()+datetime.timedelta(days=180)) @@ -934,6 +956,24 @@ def submit_new_individual(self, formats): self.assertEqual(new_revision.by.name, "Submitter Name") self.verify_bibxml_ids_creation(draft) + repository_path = Path(draft.get_file_name()) + self.assertTrue(repository_path.exists()) # Note that this doesn't check that it has the right _content_ + ftp_path = Path(settings.FTP_DIR) / "internet-drafts" / repository_path.name + self.assertTrue(repository_path.samefile(ftp_path)) + all_archive_path = Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR) / repository_path.name + self.assertTrue(repository_path.samefile(all_archive_path)) + for ext in settings.IDSUBMIT_FILE_TYPES: + if ext == "txt": + continue + variant_path = repository_path.parent / f"{repository_path.stem}.{ext}" + if variant_path.exists(): + variant_ftp_path = Path(settings.FTP_DIR) / "internet-drafts" / variant_path.name + self.assertTrue(variant_path.samefile(variant_ftp_path)) + variant_all_archive_path = Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR) / variant_path.name + self.assertTrue(variant_path.samefile(variant_all_archive_path)) + + + def test_submit_new_individual_txt(self): self.submit_new_individual(["txt"]) @@ -1815,7 +1855,7 @@ def test_submit_invalid_yang(self): # m = q('#yang-validation-message').text() for command in ['xym', 'pyang', 'yanglint']: - version = VersionInfo.objects.get(command=command).version + version = tool_version[command] if command != 'yanglint' or (settings.SUBMIT_YANGLINT_COMMAND and os.path.exists(settings.YANGLINT_BINARY)): self.assertIn(version, m) self.assertIn("draft-yang-testing-invalid-00.txt", m) @@ -2293,451 +2333,6 @@ def test_cancel_preapproval(self): self.assertEqual(len(Preapproval.objects.filter(name=preapproval.name)), 0) -class ManualPostsTestCase(BaseSubmitTestCase): - def test_manual_posts(self): - GroupFactory(acronym='mars') - - url = urlreverse('ietf.submit.views.manualpost') - # Secretariat has access - self.client.login(username="secretary", password="secretary+password") - - Submission.objects.create(name="draft-ietf-mars-foo", - group=Group.objects.get(acronym="mars"), - submission_date=date_today(), - state_id="manual") - Submission.objects.create(name="draft-ietf-mars-bar", - group=Group.objects.get(acronym="mars"), - submission_date=date_today(), - rev="00", - state_id="grp-appr") - - # get - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - - self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-foo")')), 1) - self.assertEqual(len(q('.submissions a:contains("draft-ietf-mars-bar")')), 0) - - def test_waiting_for_draft(self): - message_string = """To: somebody@ietf.org -From: joe@test.com -Date: {} -Subject: test submission via email - -Please submit my draft at http://test.com/mydraft.txt - -Thank you -""".format(timezone.now().ctime()) - message = email.message_from_string(force_str(message_string)) - submission, submission_email_event = ( - add_submission_email(request=None, - remote_ip ="192.168.0.1", - name = "draft-my-new-draft", - rev='00', - submission_pk=None, - message = message, - by = Person.objects.get(name="(System)"), - msgtype = "msgin") ) - - url = urlreverse('ietf.submit.views.manualpost') - # Secretariat has access - self.client.login(username="secretary", password="secretary+password") - - # get - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - - self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 1) - - # Same name should raise an error - with self.assertRaises(Exception): - add_submission_email(request=None, - remote_ip ="192.168.0.1", - name = "draft-my-new-draft", - rev='00', - submission_pk=None, - message = message, - by = Person.objects.get(name="(System)"), - msgtype = "msgin") - - # Cancel this one - r = self.client.post(urlreverse("ietf.submit.views.cancel_waiting_for_draft"), { - "submission_id": submission.pk, - "access_token": submission.access_token(), - }) - self.assertEqual(r.status_code, 302) - url = r["Location"] - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertEqual(len(q('.waiting-for-draft a:contains("draft-my-new-draft")')), 0) - - # Should now be able to add it again - submission, submission_email_event = ( - add_submission_email(request=None, - remote_ip ="192.168.0.1", - name = "draft-my-new-draft", - rev='00', - submission_pk=None, - message = message, - by = Person.objects.get(name="(System)"), - msgtype = "msgin") ) - - - def test_waiting_for_draft_with_attachment(self): - frm = "joe@test.com" - - message_string = """To: somebody@ietf.org -From: {} -Date: {} -Subject: A very important message with a small attachment -Content-Type: multipart/mixed; boundary="------------090908050800030909090207" - -This is a multi-part message in MIME format. ---------------090908050800030909090207 -Content-Type: text/plain; charset=utf-8; format=flowed -Content-Transfer-Encoding: 7bit - -The message body will probably say something about the attached document - ---------------090908050800030909090207 -Content-Type: text/plain; charset=UTF-8; name="attach.txt" -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="attach.txt" - -QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs -ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg== ---------------090908050800030909090207-- -""".format(frm, timezone.now().ctime()) - - message = email.message_from_string(force_str(message_string)) - submission, submission_email_event = ( - add_submission_email(request=None, - remote_ip ="192.168.0.1", - name = "draft-my-new-draft", - rev='00', - submission_pk=None, - message = message, - by = Person.objects.get(name="(System)"), - msgtype = "msgin") ) - - manualpost_page_url = urlreverse('ietf.submit.views.manualpost') - # Secretariat has access - self.client.login(username="secretary", password="secretary+password") - - self.check_manualpost_page(submission=submission, - submission_email_event=submission_email_event, - the_url=manualpost_page_url, - submission_name_fragment='draft-my-new-draft', - frm=frm, - is_secretariat=True) - - # Try the status page with no credentials - self.client.logout() - - self.check_manualpost_page(submission=submission, - submission_email_event=submission_email_event, - the_url=manualpost_page_url, - submission_name_fragment='draft-my-new-draft', - frm=frm, - is_secretariat=False) - - # Post another message to this submission using the link - message_string = """To: somebody@ietf.org -From: joe@test.com -Date: {} -Subject: A new submission message with a small attachment -Content-Type: multipart/mixed; boundary="------------090908050800030909090207" - -This is a multi-part message in MIME format. ---------------090908050800030909090207 -Content-Type: text/plain; charset=utf-8; format=flowed -Content-Transfer-Encoding: 7bit - -The message body will probably say something more about the attached document - ---------------090908050800030909090207 -Content-Type: text/plain; charset=UTF-8; name="attach.txt" -Content-Transfer-Encoding: base64 -Content-Disposition: attachment; filename="attachment.txt" - -QW4gZXhhbXBsZSBhdHRhY2htZW50IHd0aG91dCB2ZXJ5IG11Y2ggaW4gaXQuCgpBIGNvdXBs -ZSBvZiBsaW5lcyAtIGJ1dCBpdCBjb3VsZCBiZSBhIGRyYWZ0Cg== ---------------090908050800030909090207-- -""".format(timezone.now().ctime()) - - # Back to secretariat - self.client.login(username="secretary", password="secretary+password") - - r, q = self.request_and_parse(manualpost_page_url) - - url = self.get_href(q, "a#new-submission-email:contains('New submission from email')") - - # Get the form - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - #self.assertEqual(len(q('input[name=edit-title]')), 1) - - # Post the new message - r = self.client.post(url, { - "name": "draft-my-next-new-draft-00", - "direction": "incoming", - "message": message_string, - }) - - if r.status_code != 302: - q = PyQuery(r.content) - print(q) - - self.assertEqual(r.status_code, 302) - - - #self.check_manualpost_page(submission, submission_email_event, - # url, 'draft-my-next-new-draft' - # 'Another very important message', - # true) - - def check_manualpost_page(self, submission, submission_email_event, - the_url, submission_name_fragment, - frm, - is_secretariat): - # get the page listing manual posts - r, q = self.request_and_parse(the_url) - selector = "#waiting-for-draft a#add-submission-email%s:contains('Add email')" % submission.pk - - if is_secretariat: - # Can add an email to the submission - add_email_url = self.get_href(q, selector) - else: - # No add email button button - self.assertEqual(len(q(selector)), 0) - - # Find the link for our submission in those awaiting drafts - submission_url = self.get_href(q, "#waiting-for-draft a#aw{}:contains('{}')". - format(submission.pk, submission_name_fragment)) - - # Follow the link to the status page for this submission - r, q = self.request_and_parse(submission_url) - - selector = "#history a#reply%s:contains('Reply')" % submission.pk - - if is_secretariat: - # check that reply button is visible and get the form - reply_url = self.get_href(q, selector) - - # Get the form - r = self.client.get(reply_url) - self.assertEqual(r.status_code, 200) - reply_q = PyQuery(r.content) - self.assertEqual(len(reply_q('input[name=to]')), 1) - else: - # No reply button - self.assertEqual(len(q(selector)), 0) - - if is_secretariat: - # Now try to send an email using the send email link - - selector = "a#send%s:contains('Send Email')" % submission.pk - send_url = self.get_href(q, selector) - - self.do_submission_email(the_url = send_url, - to = frm, - body = "A new message") - - # print q - # print submission.pk - # print submission_email_event.pk - - # Find the link for our message in the list - url = self.get_href(q, "#aw{}-{}:contains('{}')".format(submission.pk, - submission_email_event.message.pk, - "Received message - manual post")) - - # Page displaying message details - r, q = self.request_and_parse(url) - - if is_secretariat: - # check that reply button is visible - - reply_href = self.get_href(q, "a#reply%s:contains('Reply')" % submission.pk) - - else: - # No reply button - self.assertEqual(len(q(selector)), 0) - reply_href = None - - # check that attachment link is visible - - url = self.get_href(q, "#email-details a#attach{}:contains('attach.txt')".format(submission.pk)) - - # Fetch the attachment - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - - # Attempt a reply if we can - if reply_href == None: - return - - self.do_submission_email(the_url = reply_href, - to = frm, - body = "A reply to the message") - - # try adding an email to the submission - # Use the add email link from the manual post listing page - - if is_secretariat: - # Can add an email to the submission - # add_email_url set previously - r = self.client.get(add_email_url) - self.assertEqual(r.status_code, 200) - add_email_q = PyQuery(r.content) - self.assertEqual(len(add_email_q('input[name=submission_pk]')), 1) - - # Add a simple email - new_message_string = """To: somebody@ietf.org -From: joe@test.com -Date: {} -Subject: Another message - -About my submission - -Thank you -""".format(timezone.now().ctime()) - - r = self.client.post(add_email_url, { - "name": "{}-{}".format(submission.name, submission.rev), - "direction": "incoming", - "submission_pk": submission.pk, - "message": new_message_string, - }) - - if r.status_code != 302: - q = PyQuery(r.content) - print(q) - - self.assertEqual(r.status_code, 302) - - def request_and_parse(self, url): - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - return r, PyQuery(r.content) - - - def get_href(self, q, query): - link = q(query) - self.assertEqual(len(link), 1) - - return PyQuery(link[0]).attr('href') - - - def do_submission_email(self, the_url, to, body): - # check the page - r = self.client.get(the_url) - q = PyQuery(r.content) - post_button = q('[type=submit]:contains("Send email")') - self.assertEqual(len(post_button), 1) - subject = post_button.parents("form").find('input[name="subject"]').val() - frm = post_button.parents("form").find('input[name="frm"]').val() - cc = post_button.parents("form").find('input[name="cc"]').val() - reply_to = post_button.parents("form").find('input[name="reply_to"]').val() - - empty_outbox() - - # post submitter info - r = self.client.post(the_url, { - "subject": subject, - "frm": frm, - "to": to, - "cc": cc, - "reply_to": reply_to, - "body": body, - }) - - self.assertEqual(r.status_code, 302) - - self.assertEqual(len(outbox), 1) - - outmsg = outbox[0] - self.assertTrue(to in outmsg['To']) - - reply_to = outmsg['Reply-To'] - self.assertIsNotNone(reply_to, "Expected Reply-To") - - # Build a reply - - message_string = """To: {} -From: {} -Date: {} -Subject: test -""".format(reply_to, to, timezone.now().ctime()) - - result = process_response_email(message_string) - self.assertIsInstance(result, Message) - - return r - - def do_submission(self, name, rev, group=None, formats=["txt",]): - # We're not testing the submission process - just the submission status - - # get - url = urlreverse('ietf.submit.views.upload_submission') - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - q = PyQuery(r.content) - self.assertEqual(len(q('input[type=file][name=txt]')), 1) - self.assertEqual(len(q('input[type=file][name=xml]')), 1) - - # submit - files = {} - for format in formats: - files[format], author = submission_file(f'{name}-{rev}', f'{name}-{rev}.{format}', group, "test_submission.%s" % format) - - r = self.post_to_upload_submission(url, files) - if r.status_code != 302: - q = PyQuery(r.content) - print(q('div.invalid-feedback span.form-text div').text()) - - self.assertEqual(r.status_code, 302) - - status_url = r["Location"] - for format in formats: - self.assertTrue(os.path.exists(os.path.join(self.staging_dir, "%s-%s.%s" % (name, rev, format)))) - self.assertEqual(Submission.objects.filter(name=name).count(), 1) - submission = Submission.objects.get(name=name) - self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ])) - self.assertEqual(len(submission.authors), 1) - author = submission.authors[0] - self.assertEqual(author["name"], "Author Name") - self.assertEqual(author["email"], "author@example.com") - - return status_url - - - def supply_extra_metadata(self, name, status_url, submitter_name, submitter_email): - # check the page - r = self.client.get(status_url) - q = PyQuery(r.content) - post_button = q('[type=submit]:contains("Post")') - self.assertEqual(len(post_button), 1) - action = post_button.parents("form").find('input[type=hidden][name="action"]').val() - - # post submitter info - r = self.client.post(status_url, { - "action": action, - "submitter-name": submitter_name, - "submitter-email": submitter_email, - "approvals_received": True, - }) - - if r.status_code == 302: - submission = Submission.objects.get(name=name) - self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email))) - - return r - # Transaction.on_commit() requires use of TransactionTestCase, but that has a performance penalty. Replace it # with a no-op for testing purposes. @@ -3542,28 +3137,59 @@ def test_status_of_validating_submission(self): self.assertContains(r, s.name) self.assertContains(r, 'This submission is being processed and validated.', status_code=200) - @override_settings(IDSUBMIT_MAX_VALIDATION_TIME=datetime.timedelta(minutes=30)) + @override_settings( + IDSUBMIT_MAX_VALIDATION_TIME=datetime.timedelta(minutes=30), + IDSUBMIT_EXPIRATION_AGE=datetime.timedelta(minutes=90), + ) def test_cancel_stale_submissions(self): + # these will be lists of (Submission, "state_id") pairs + submissions_to_skip = [] + submissions_to_cancel = [] + + # submissions in the validating state fresh_submission = SubmissionFactory(state_id='validating') fresh_submission.submissionevent_set.create( desc='fake created event', time=timezone.now() - datetime.timedelta(minutes=15), ) + submissions_to_skip.append((fresh_submission, "validating")) + stale_submission = SubmissionFactory(state_id='validating') stale_submission.submissionevent_set.create( desc='fake created event', time=timezone.now() - datetime.timedelta(minutes=30, seconds=1), ) + submissions_to_cancel.append((stale_submission, "validating")) + + # submissions in other states + for state in DraftSubmissionStateName.objects.filter(used=True).exclude(slug="validating"): + to_skip = SubmissionFactory(state_id=state.pk) + to_skip.submissionevent_set.create( + desc="fake created event", + time=timezone.now() - datetime.timedelta(minutes=45), # would be canceled if it were "validating" + ) + submissions_to_skip.append((to_skip, state.pk)) + to_expire = SubmissionFactory(state_id=state.pk) + to_expire.submissionevent_set.create( + desc="fake created event", + time=timezone.now() - datetime.timedelta(minutes=90, seconds=1), + ) + if state.pk in ["posted", "cancel"]: + submissions_to_skip.append((to_expire, state.pk)) # these ones should not be expired regardless of age + else: + submissions_to_cancel.append(((to_expire, state.pk))) cancel_stale_submissions() - fresh_submission = Submission.objects.get(pk=fresh_submission.pk) - self.assertEqual(fresh_submission.state_id, 'validating') - self.assertEqual(fresh_submission.submissionevent_set.count(), 1) + for _subm, original_state_id in submissions_to_skip: + subm = Submission.objects.get(pk=_subm.pk) + self.assertEqual(subm.state_id, original_state_id) + self.assertEqual(subm.submissionevent_set.count(), 1) - stale_submission = Submission.objects.get(pk=stale_submission.pk) - self.assertEqual(stale_submission.state_id, 'cancel') - self.assertEqual(stale_submission.submissionevent_set.count(), 2) + for _subm, _ in submissions_to_cancel: + subm = Submission.objects.get(pk=_subm.pk) + self.assertEqual(subm.state_id, "cancel") + self.assertEqual(subm.submissionevent_set.count(), 2) class ApiSubmitTests(BaseSubmitTestCase): @@ -3862,3 +3488,28 @@ def test_submission_checks(self): "Your Internet-Draft failed at least one submission check.", status_code=200, ) + + +class YangCheckerTests(TestCase): + @mock.patch("ietf.submit.utils.apply_yang_checker_to_draft") + def test_run_all_yang_model_checks(self, mock_apply): + active_drafts = WgDraftFactory.create_batch(3) + WgDraftFactory(states=[("draft", "expired")]) + run_all_yang_model_checks() + self.assertEqual(mock_apply.call_count, 3) + self.assertCountEqual( + [args[0][1] for args in mock_apply.call_args_list], + active_drafts, + ) + + def test_apply_yang_checker_to_draft(self): + draft = WgDraftFactory() + submission = SubmissionFactory(name=draft.name, rev=draft.rev) + submission.checks.create(checker="my-checker") + checker = mock.Mock() + checker.name = "my-checker" + checker.symbol = "X" + checker.check_file_txt.return_value = (True, "whee", None, None, {}) + apply_yang_checker_to_draft(checker, draft) + self.assertEqual(checker.check_file_txt.call_args, mock.call(draft.get_file_name())) + diff --git a/ietf/submit/urls.py b/ietf/submit/urls.py index 2309ec55cd..4f53103c2c 100644 --- a/ietf/submit/urls.py +++ b/ietf/submit/urls.py @@ -17,15 +17,6 @@ url(r'^approvals/cancelpreapproval/(?P[a-f\d]+)/$', views.cancel_preapproval), url(r'^manualpost/$', views.manualpost), - url(r'^manualpost/addemail$', views.add_manualpost_email), - url(r'^manualpost/addemail/(?P\d+)/(?P[a-f\d]*)/$', views.add_manualpost_email), - url(r'^manualpost/attachment/(?P\d+)/(?P\d+)/(?P.*)$', views.show_submission_email_attachment), - url(r'^manualpost/cancel$', views.cancel_waiting_for_draft), - url(r'^manualpost/email/(?P\d+)/(?P\d+)/$', views.show_submission_email_message), - url(r'^manualpost/email/(?P\d+)/(?P\d+)/(?P[a-f\d]*)/$', views.show_submission_email_message), - url(r'^manualpost/replyemail/(?P\d+)/(?P\d+)/$', views.send_submission_email), - url(r'^manualpost/sendemail/(?P\d+)/$', views.send_submission_email), - # proof-of-concept for celery async tasks url(r'^async-poke/?$', views.async_poke_test), -] \ No newline at end of file +] diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index e03c702632..c814b84657 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -4,9 +4,11 @@ import datetime import io +import json import os import pathlib import re +import sys import time import traceback import xml2rfc @@ -15,6 +17,7 @@ from shutil import move from typing import Optional, Union # pyflakes:ignore from unidecode import unidecode +from xym import xym from django.conf import settings from django.core.exceptions import ValidationError @@ -43,6 +46,7 @@ from ietf.community.utils import update_name_contains_indexes_with_new_doc from ietf.submit.mail import ( announce_to_lists, announce_new_version, announce_to_authors, send_approval_request, send_submission_confirmation, announce_new_wg_00, send_manual_post_request ) +from ietf.submit.checkers import DraftYangChecker from ietf.submit.models import ( Submission, SubmissionEvent, Preapproval, DraftSubmissionStateName, SubmissionCheck, SubmissionExtResource ) from ietf.utils import log @@ -167,7 +171,10 @@ def validate_submission_rev(name, rev): if rev != expected: return 'Invalid revision (revision %02d is expected)' % expected - + + # This is not really correct, though the edges that it doesn't cover are not likely. + # It might be better just to look in the combined archive to make sure we're not colliding with + # a thing that exists there already because it was included from an approved personal collection. for dirname in [settings.INTERNET_DRAFT_PATH, settings.INTERNET_DRAFT_ARCHIVE_DIR, ]: dir = pathlib.Path(dirname) pattern = '%s-%02d.*' % (name, rev) @@ -652,9 +659,13 @@ def move_files_to_repository(submission): dest = Path(settings.IDSUBMIT_REPOSITORY_PATH) / fname if source.exists(): move(source, dest) + all_archive_dest = Path(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR) / dest.name + ftp_dest = Path(settings.FTP_DIR) / "internet-drafts" / dest.name + os.link(dest, all_archive_dest) + os.link(dest, ftp_dest) elif dest.exists(): log.log("Intended to move '%s' to '%s', but found source missing while destination exists.") - elif ext in submission.file_types.split(','): + elif f".{ext}" in submission.file_types.split(','): raise ValueError("Intended to move '%s' to '%s', but found source and destination missing.") @@ -1424,3 +1435,133 @@ def process_uploaded_submission(submission): submission.state_id = "uploaded" submission.save() create_submission_event(None, submission, desc="Completed submission validation checks") + + +def apply_yang_checker_to_draft(checker, draft): + submission = Submission.objects.filter(name=draft.name, rev=draft.rev).order_by('-id').first() + if submission: + check = submission.checks.filter(checker=checker.name).order_by('-id').first() + if check: + result = checker.check_file_txt(draft.get_file_name()) + passed, message, errors, warnings, items = result + items = json.loads(json.dumps(items)) + new_res = (passed, errors, warnings, message) + old_res = (check.passed, check.errors, check.warnings, check.message) if check else () + if new_res != old_res: + log.log(f"Saving new yang checker results for {draft.name}-{draft.rev}") + qs = submission.checks.filter(checker=checker.name).order_by('time') + submission.checks.filter(checker=checker.name).exclude(pk=qs.first().pk).delete() + submission.checks.create(submission=submission, checker=checker.name, passed=passed, + message=message, errors=errors, warnings=warnings, items=items, + symbol=checker.symbol) + else: + log.log(f"Could not run yang checker for {draft.name}-{draft.rev}: missing submission object") + + +def run_all_yang_model_checks(): + checker = DraftYangChecker() + for draft in Document.objects.filter( + type_id="draft", + states=State.objects.get(type="draft", slug="active"), + ): + apply_yang_checker_to_draft(checker, draft) + + +def populate_yang_model_dirs(): + """Update the yang model dirs + + * All yang modules from published RFCs should be extracted and be + available in an rfc-yang repository. + + * All valid yang modules from active, not replaced, Internet-Drafts + should be extracted and be available in a draft-valid-yang repository. + + * All, valid and invalid, yang modules from active, not replaced, + Internet-Drafts should be available in a draft-all-yang repository. + (Actually, given precedence ordering, it would be enough to place + non-validating modules in a draft-invalid-yang repository instead). + + * In all cases, example modules should be excluded. + + * Precedence is established by the search order of the repository as + provided to pyang. + + * As drafts expire, models should be removed in order to catch cases + where a module being worked on depends on one which has slipped out + of the work queue. + + """ + def extract_from(file, dir, strict=True): + saved_stdout = sys.stdout + saved_stderr = sys.stderr + xymerr = io.StringIO() + xymout = io.StringIO() + sys.stderr = xymerr + sys.stdout = xymout + model_list = [] + try: + model_list = xym.xym(str(file), str(file.parent), str(dir), strict=strict, debug_level=-2) + for name in model_list: + modfile = moddir / name + mtime = file.stat().st_mtime + os.utime(str(modfile), (mtime, mtime)) + if '"' in name: + name = name.replace('"', '') + modfile.rename(str(moddir / name)) + model_list = [n.replace('"', '') for n in model_list] + except Exception as e: + log.log("Error when extracting from %s: %s" % (file, str(e))) + finally: + sys.stdout = saved_stdout + sys.stderr = saved_stderr + return model_list + + # Extract from new RFCs + + rfcdir = Path(settings.RFC_PATH) + + moddir = Path(settings.SUBMIT_YANG_RFC_MODEL_DIR) + if not moddir.exists(): + moddir.mkdir(parents=True) + + latest = 0 + for item in moddir.iterdir(): + if item.stat().st_mtime > latest: + latest = item.stat().st_mtime + + log.log(f"Extracting RFC Yang models to {moddir} ...") + for item in rfcdir.iterdir(): + if item.is_file() and item.name.startswith('rfc') and item.name.endswith('.txt') and item.name[3:-4].isdigit(): + if item.stat().st_mtime > latest: + model_list = extract_from(item, moddir) + for name in model_list: + if not (name.startswith('ietf') or name.startswith('iana')): + modfile = moddir / name + modfile.unlink() + + # Extract valid modules from drafts + + six_months_ago = time.time() - 6 * 31 * 24 * 60 * 60 + + def active(dirent): + return dirent.stat().st_mtime > six_months_ago + + draftdir = Path(settings.INTERNET_DRAFT_PATH) + moddir = Path(settings.SUBMIT_YANG_DRAFT_MODEL_DIR) + if not moddir.exists(): + moddir.mkdir(parents=True) + log.log(f"Emptying {moddir} ...") + for item in moddir.iterdir(): + item.unlink() + + log.log(f"Extracting draft Yang models to {moddir} ...") + for item in draftdir.iterdir(): + try: + if item.is_file() and item.name.startswith('draft') and item.name.endswith('.txt') and active(item): + model_list = extract_from(item, moddir, strict=False) + for name in model_list: + if name.startswith('example'): + modfile = moddir / name + modfile.unlink() + except UnicodeDecodeError as e: + log.log(f"Error processing {item.name}: {e}") diff --git a/ietf/submit/views.py b/ietf/submit/views.py index b583a53fc4..6f23ba49d4 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -3,7 +3,6 @@ import re -import base64 import datetime from typing import Optional, cast # pyflakes:ignore @@ -22,31 +21,30 @@ import debug # pyflakes:ignore -from ietf.doc.models import Document, AddedMessageEvent +from ietf.doc.models import Document from ietf.doc.forms import ExtResourceForm from ietf.group.models import Group from ietf.group.utils import group_features_group_filter from ietf.ietfauth.utils import has_role, role_required from ietf.mailtrigger.utils import gather_address_lists -from ietf.message.models import Message, MessageAttachment from ietf.person.models import Email from ietf.submit.forms import (SubmissionAutoUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm, - PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm, + PreapprovalForm, ReplacesForm, DeprecatedSubmissionAutoUploadForm, SubmissionManualUploadForm) -from ietf.submit.mail import send_full_url, send_manual_post_request, add_submission_email, get_reply_to +from ietf.submit.mail import send_full_url, send_manual_post_request from ietf.submit.models import (Submission, Preapproval, SubmissionExtResource, - DraftSubmissionStateName, SubmissionEmailEvent ) + DraftSubmissionStateName ) from ietf.submit.tasks import process_uploaded_submission_task, process_and_accept_uploaded_submission_task, poke from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_for_user, recently_approved_by_user, validate_submission, create_submission_event, docevent_from_submission, post_submission, cancel_submission, rename_submission_files, remove_submission_files, get_draft_meta, get_submission, fill_in_submission, apply_checkers, save_files, clear_existing_files, check_submission_revision_consistency, accept_submission, accept_submission_requires_group_approval, - accept_submission_requires_prev_auth_approval, update_submission_external_resources, remote_ip ) + accept_submission_requires_prev_auth_approval, update_submission_external_resources) from ietf.stats.utils import clean_country_name from ietf.utils.accesstoken import generate_access_token from ietf.utils.log import log -from ietf.utils.mail import parseaddr, send_mail_message +from ietf.utils.mail import parseaddr from ietf.utils.response import permission_denied from ietf.utils.timezone import date_today @@ -780,243 +778,14 @@ def manualpost(request): s.passes_checks = all([ c.passed!=False for c in s.checks.all() ]) s.errors = validate_submission(s) - waiting_for_draft = Submission.objects.filter(state_id = "waiting-for-draft").distinct() - - return render(request, 'submit/manual_post.html', - {'manual': manual, - 'selected': 'manual_posts', - 'waiting_for_draft': waiting_for_draft}) - - -def cancel_waiting_for_draft(request): - if request.method == 'POST': - can_cancel = has_role(request.user, "Secretariat") - - if not can_cancel: - permission_denied(request, 'You do not have permission to perform this action.') - - submission_id = request.POST.get('submission_id', '') - access_token = request.POST.get('access_token', '') - - submission = get_submission_or_404(submission_id, access_token = access_token) - cancel_submission(submission) - - create_submission_event(request, submission, "Cancelled submission") - if (submission.rev != "00"): - # Add a doc event - docevent_from_submission(submission, "Cancelled submission for rev {}".format(submission.rev)) - - return redirect("ietf.submit.views.manualpost") - - -@role_required('Secretariat',) -def add_manualpost_email(request, submission_id=None, access_token=None): - """Add email to submission history""" - - if request.method == 'POST': - try: - button_text = request.POST.get('submit', '') - if button_text == 'Cancel': - return redirect("submit/manual_post.html") - - form = SubmissionEmailForm(request.POST) - if form.is_valid(): - submission_pk = form.cleaned_data['submission_pk'] - message = form.cleaned_data['message'] - #in_reply_to = form.cleaned_data['in_reply_to'] - # create Message - - if form.cleaned_data['direction'] == 'incoming': - msgtype = 'msgin' - else: - msgtype = 'msgout' - - submission, submission_email_event = ( - add_submission_email(request=request, - remote_ip=remote_ip(request), - name = form.draft_name, - rev=form.revision, - submission_pk = submission_pk, - message = message, - by = request.user.person, - msgtype = msgtype) ) - - messages.success(request, 'Email added.') - - try: - draft = Document.objects.get(name=submission.name) - except Document.DoesNotExist: - # Assume this is revision 00 - we'll do this later - draft = None - - if (draft != None): - e = AddedMessageEvent(type="added_message", doc=draft) - e.message = submission_email_event.submissionemailevent.message - e.msgtype = submission_email_event.submissionemailevent.msgtype - e.in_reply_to = submission_email_event.submissionemailevent.in_reply_to - e.by = request.user.person - e.desc = submission_email_event.desc - e.time = submission_email_event.time - e.save() - - return redirect("ietf.submit.views.manualpost") - except ValidationError as e: - form = SubmissionEmailForm(request.POST) - form._errors = {} - form._errors["__all__"] = form.error_class(["There was a failure uploading your message. (%s)" % e.message]) - else: - initial = { - } - - if (submission_id != None): - submission = get_submission_or_404(submission_id, access_token) - initial['name'] = "{}-{}".format(submission.name, submission.rev) - initial['direction'] = 'incoming' - initial['submission_pk'] = submission.pk - else: - initial['direction'] = 'incoming' - - form = SubmissionEmailForm(initial=initial) - - return render(request, 'submit/add_submit_email.html',dict(form=form)) - - -@role_required('Secretariat',) -def send_submission_email(request, submission_id, message_id=None): - """Send an email related to a submission""" - submission = get_submission_or_404(submission_id, access_token = None) - - if request.method == 'POST': - button_text = request.POST.get('submit', '') - if button_text == 'Cancel': - return redirect('ietf.submit.views.submission_status', - submission_id=submission.id, - access_token=submission.access_token()) - - form = MessageModelForm(request.POST) - if form.is_valid(): - # create Message - msg = Message.objects.create( - by = request.user.person, - subject = form.cleaned_data['subject'], - frm = form.cleaned_data['frm'], - to = form.cleaned_data['to'], - cc = form.cleaned_data['cc'], - bcc = form.cleaned_data['bcc'], - reply_to = form.cleaned_data['reply_to'], - body = form.cleaned_data['body'] - ) - - in_reply_to_id = form.cleaned_data['in_reply_to_id'] - in_reply_to = None - rp = "" - - if in_reply_to_id: - rp = " reply" - try: - in_reply_to = Message.objects.get(id=in_reply_to_id) - except Message.DoesNotExist: - log("Unable to retrieve in_reply_to message: %s" % in_reply_to_id) - - desc = "Sent message {} - manual post - {}-{}".format(rp, - submission.name, - submission.rev) - SubmissionEmailEvent.objects.create( - submission = submission, - desc = desc, - msgtype = 'msgout', - by = request.user.person, - message = msg, - in_reply_to = in_reply_to) - - # send email - send_mail_message(None,msg) - - messages.success(request, 'Email sent.') - return redirect('ietf.submit.views.submission_status', - submission_id=submission.id, - access_token=submission.access_token()) - - else: - reply_to = get_reply_to() - msg = None - - if not message_id: - addrs = gather_address_lists('sub_confirmation_requested',submission=submission).as_strings(compact=False) - to_email = addrs.to - cc = addrs.cc - subject = 'Regarding {}'.format(submission.name) - else: - try: - submitEmail = SubmissionEmailEvent.objects.get(id=message_id) - msg = submitEmail.message - - if msg: - to_email = msg.frm - cc = msg.cc - subject = 'Re:{}'.format(msg.subject) - else: - to_email = None - cc = None - subject = 'Regarding {}'.format(submission.name) - except Message.DoesNotExist: - to_email = None - cc = None - subject = 'Regarding {}'.format(submission.name) - - initial = { - 'to': to_email, - 'cc': cc, - 'frm': settings.IDSUBMIT_FROM_EMAIL, - 'subject': subject, - 'reply_to': reply_to, + return render( + request, + 'submit/manual_post.html', + { + 'manual': manual, + 'selected': 'manual_posts' } - - if msg: - initial['in_reply_to_id'] = msg.id - - form = MessageModelForm(initial=initial) - - return render(request, "submit/email.html", { - 'submission': submission, - 'access_token': submission.access_token(), - 'form':form}) - - -def show_submission_email_message(request, submission_id, message_id, access_token=None): - submission = get_submission_or_404(submission_id, access_token) - - submitEmail = get_object_or_404(SubmissionEmailEvent, pk=message_id) - attachments = submitEmail.message.messageattachment_set.all() - - return render(request, 'submit/submission_email.html', - {'submission': submission, - 'message': submitEmail, - 'attachments': attachments}) - -def show_submission_email_attachment(request, submission_id, message_id, filename, access_token=None): - get_submission_or_404(submission_id, access_token) - - message = get_object_or_404(SubmissionEmailEvent, pk=message_id) - - attach = get_object_or_404(MessageAttachment, - message=message.message, - filename=filename) - - if attach.encoding == "base64": - body = base64.b64decode(attach.body) - else: - body = attach.body.encode('utf-8') - - if attach.content_type is None: - content_type='text/plain' - else: - content_type=attach.content_type - - response = HttpResponse(body, content_type=content_type) - response['Content-Disposition'] = 'attachment; filename=%s' % attach.filename - response['Content-Length'] = len(body) - return response + ) def get_submission_or_404(submission_id, access_token=None): diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 9ce54a687b..f46fe407d4 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -304,3 +304,22 @@ def add_review_comment(doc_name, review_time, by, comment): e.by = by e.save() + + +def ingest_review_email(message: bytes): + from ietf.api.views import EmailIngestionError # avoid circular import + try: + doc_name, review_time, by, comment = parse_review_email(message) + except Exception as err: + raise EmailIngestionError("Unable to parse message as IANA review email") from err + log(f"Read IANA review email for {doc_name} at {review_time} by {by}") + if by.name == "(System)": + log("WARNING: person responsible for email does not have a IANA role") # (sic) + try: + add_review_comment(doc_name, review_time, by, comment) + except Document.DoesNotExist: + log(f"ERROR: unknown document {doc_name}") + raise EmailIngestionError(f"Unknown document {doc_name}") + except Exception as err: + raise EmailIngestionError("Error ingesting IANA review email") from err + diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index 8ae1354139..d3371ea36c 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -802,6 +802,10 @@ def post_approved_draft(url, name): the data from the Datatracker and start processing it. Returns response and error (empty string if no error).""" + if settings.SERVER_MODE != "production": + log(f"In production, would have posted RFC-Editor notification of approved I-D '{name}' to '{url}'") + return "", "" + # HTTP basic auth username = "dtracksync" password = settings.RFC_EDITOR_SYNC_PASSWORD diff --git a/ietf/sync/tasks.py b/ietf/sync/tasks.py index bc1218601f..53e23d7913 100644 --- a/ietf/sync/tasks.py +++ b/ietf/sync/tasks.py @@ -13,6 +13,7 @@ from ietf.sync import iana from ietf.sync import rfceditor +from ietf.sync.rfceditor import MIN_QUEUE_RESULTS, parse_queue, update_drafts_from_queue from ietf.utils import log from ietf.utils.timezone import date_today @@ -70,6 +71,33 @@ def rfc_editor_index_update_task(full_index=False): log.log("RFC%s, %s: %s" % (rfc_number, doc.name, c)) +@shared_task +def rfc_editor_queue_updates_task(): + log.log(f"Updating RFC Editor queue states from {settings.RFC_EDITOR_QUEUE_URL}") + try: + response = requests.get( + settings.RFC_EDITOR_QUEUE_URL, + timeout=30, # seconds + ) + except requests.Timeout as exc: + log.log(f"GET request timed out retrieving RFC editor queue: {exc}") + return # failed + drafts, warnings = parse_queue(io.StringIO(response.text)) + for w in warnings: + log.log(f"Warning: {w}") + + if len(drafts) < MIN_QUEUE_RESULTS: + log.log("Not enough results, only %s" % len(drafts)) + return # failed + + changed, warnings = update_drafts_from_queue(drafts) + for w in warnings: + log.log(f"Warning: {w}") + + for c in changed: + log.log(f"Updated {c}") + + @shared_task def iana_changes_update_task(): # compensate to avoid we ask for something that happened now and then diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index fec353a97c..b0cdf863f0 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -19,10 +19,12 @@ import debug # pyflakes:ignore +from ietf.api.views import EmailIngestionError from ietf.doc.factories import WgDraftFactory, RfcFactory, DocumentAuthorFactory, DocEventFactory from ietf.doc.models import Document, DocEvent, DeletedEvent, DocTagName, RelatedDocument, State, StateDocEvent from ietf.doc.utils import add_state_change_event from ietf.group.factories import GroupFactory +from ietf.person.factories import PersonFactory from ietf.person.models import Person from ietf.sync import iana, rfceditor, tasks from ietf.utils.mail import outbox, empty_outbox @@ -214,6 +216,61 @@ def test_iana_review_mail(self): iana.add_review_comment(doc_name, review_time, by, comment) self.assertEqual(DocEvent.objects.filter(doc=draft, type="iana_review").count(), events_before+1) + @mock.patch("ietf.sync.iana.add_review_comment") + @mock.patch("ietf.sync.iana.parse_review_email") + def test_ingest_review_email(self, mock_parse_review_email, mock_add_review_comment): + mock_parse_review_email.side_effect = ValueError("ouch!") + message = b"message" + + # Error parsing mail + with self.assertRaises(EmailIngestionError) as context: + iana.ingest_review_email(message) + self.assertIsNone(context.exception.as_emailmessage()) # no email + self.assertEqual("Unable to parse message as IANA review email", str(context.exception)) + self.assertTrue(mock_parse_review_email.called) + self.assertEqual(mock_parse_review_email.call_args, mock.call(message)) + self.assertFalse(mock_add_review_comment.called) + mock_parse_review_email.reset_mock() + + args = ( + "doc-name", + datetime.datetime.now(tz=datetime.timezone.utc), + PersonFactory(), + "yadda yadda yadda", + ) + mock_parse_review_email.side_effect = None + mock_parse_review_email.return_value = args + mock_add_review_comment.side_effect = Document.DoesNotExist + with self.assertRaises(EmailIngestionError) as context: + iana.ingest_review_email(message) + self.assertIsNone(context.exception.as_emailmessage()) # no email + self.assertEqual(str(context.exception), "Unknown document doc-name") + self.assertTrue(mock_parse_review_email.called) + self.assertEqual(mock_parse_review_email.call_args, mock.call(message)) + self.assertTrue(mock_add_review_comment.called) + self.assertEqual(mock_add_review_comment.call_args, mock.call(*args)) + mock_parse_review_email.reset_mock() + mock_add_review_comment.reset_mock() + + mock_add_review_comment.side_effect = ValueError("ouch!") + with self.assertRaises(EmailIngestionError) as context: + iana.ingest_review_email(message) + self.assertIsNone(context.exception.as_emailmessage()) # no email + self.assertEqual("Error ingesting IANA review email", str(context.exception)) + self.assertTrue(mock_parse_review_email.called) + self.assertEqual(mock_parse_review_email.call_args, mock.call(message)) + self.assertTrue(mock_add_review_comment.called) + self.assertEqual(mock_add_review_comment.call_args, mock.call(*args)) + mock_parse_review_email.reset_mock() + mock_add_review_comment.reset_mock() + + mock_add_review_comment.side_effect = None + iana.ingest_review_email(message) + self.assertTrue(mock_parse_review_email.called) + self.assertEqual(mock_parse_review_email.call_args, mock.call(message)) + self.assertTrue(mock_add_review_comment.called) + self.assertEqual(mock_add_review_comment.call_args, mock.call(*args)) + def test_notify_page(self): # check that we can get the notify page url = urlreverse("ietf.sync.views.notify", kwargs=dict(org="iana", notification="changes")) @@ -597,6 +654,29 @@ def test_update_draft_auth48_url(self): auth48_docurl = draft.documenturl_set.filter(tag_id='auth48').first() self.assertIsNone(auth48_docurl) + def test_post_approved_draft_in_production_only(self): + self.requests_mock.post("https://rfceditor.example.com/", status_code=200, text="OK") + + # be careful playing with SERVER_MODE! + with override_settings(SERVER_MODE="test"): + self.assertEqual( + rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"), + ("", "") + ) + self.assertFalse(self.requests_mock.called) + with override_settings(SERVER_MODE="development"): + self.assertEqual( + rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"), + ("", "") + ) + self.assertFalse(self.requests_mock.called) + with override_settings(SERVER_MODE="production"): + self.assertEqual( + rfceditor.post_approved_draft("https://rfceditor.example.com/", "some-draft"), + ("", "") + ) + self.assertTrue(self.requests_mock.called) + class DiscrepanciesTests(TestCase): def test_discrepancies(self): @@ -636,6 +716,7 @@ def test_discrepancies(self): r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) self.assertContains(r, doc.name) + class RFCEditorUndoTests(TestCase): def test_rfceditor_undo(self): draft = WgDraftFactory() @@ -805,6 +886,36 @@ def json(self): tasks.rfc_editor_index_update_task(full_index=False) self.assertFalse(update_docs_mock.called) + @override_settings(RFC_EDITOR_QUEUE_URL="https://rfc-editor.example.com/queue/") + @mock.patch("ietf.sync.tasks.update_drafts_from_queue") + @mock.patch("ietf.sync.tasks.parse_queue") + def test_rfc_editor_queue_updates_task(self, mock_parse, mock_update): + # test a request timeout + self.requests_mock.get("https://rfc-editor.example.com/queue/", exc=requests.exceptions.Timeout) + tasks.rfc_editor_queue_updates_task() + self.assertFalse(mock_parse.called) + self.assertFalse(mock_update.called) + + # now return a value rather than an exception + self.requests_mock.get("https://rfc-editor.example.com/queue/", text="the response") + + # mock returning < MIN_QUEUE_RESULTS values - treated as an error, so no update takes place + mock_parse.return_value = ([n for n in range(rfceditor.MIN_QUEUE_RESULTS - 1)], ["a warning"]) + tasks.rfc_editor_queue_updates_task() + self.assertEqual(mock_parse.call_count, 1) + self.assertEqual(mock_parse.call_args[0][0].read(), "the response") + self.assertFalse(mock_update.called) + mock_parse.reset_mock() + + # mock returning +. MIN_QUEUE_RESULTS - should succeed + mock_parse.return_value = ([n for n in range(rfceditor.MIN_QUEUE_RESULTS)], ["a warning"]) + mock_update.return_value = ([1,2,3], ["another warning"]) + tasks.rfc_editor_queue_updates_task() + self.assertEqual(mock_parse.call_count, 1) + self.assertEqual(mock_parse.call_args[0][0].read(), "the response") + self.assertEqual(mock_update.call_count, 1) + self.assertEqual(mock_update.call_args, mock.call([n for n in range(rfceditor.MIN_QUEUE_RESULTS)])) + @override_settings(IANA_SYNC_CHANGES_URL="https://iana.example.com/sync/") @mock.patch("ietf.sync.tasks.iana.update_history_with_changes") @mock.patch("ietf.sync.tasks.iana.parse_changes_json") diff --git a/ietf/sync/views.py b/ietf/sync/views.py index 30b2a928e5..a2b5f62427 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -1,9 +1,7 @@ # Copyright The IETF Trust 2012-2020, All Rights Reserved # -*- coding: utf-8 -*- - import datetime -import subprocess import os import json @@ -54,9 +52,8 @@ def notify(request, org, notification): password = request.POST.get("password") or request.GET.get("password") if username != None and password != None: - if settings.SERVER_MODE == "production" and not request.is_secure(): - permission_denied(request, "You must use HTTPS when sending username/password.") - + # Used to reject non-https traffic here, but that's now enforced by a domain-wide upgrade + # from http to https. Django's request.is_secure() is always False now. if not user.is_authenticated: try: user = User.objects.get(username__iexact=username) @@ -80,30 +77,18 @@ def notify(request, org, notification): raise Http404 if request.method == "POST": - def runscript(name): - python = os.path.join(os.path.dirname(settings.BASE_DIR), "env", "bin", "python") - cmd = [python, os.path.join(SYNC_BIN_PATH, name)] - cmdstring = " ".join(cmd) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - out, err = p.communicate() - out = out.decode('utf-8') - err = err.decode('utf-8') - if p.returncode: - log("Subprocess error %s when running '%s': %s %s" % (p.returncode, cmd, err, out)) - raise subprocess.CalledProcessError(p.returncode, cmdstring, "\n".join([err, out])) - if notification == "index": log("Queuing RFC Editor index sync from notify view POST") tasks.rfc_editor_index_update_task.delay() + elif notification == "queue": + log("Queuing RFC Editor queue sync from notify view POST") + tasks.rfc_editor_queue_updates_task.delay() elif notification == "changes": log("Queuing IANA changes sync from notify view POST") tasks.iana_changes_update_task.delay() elif notification == "protocols": log("Queuing IANA protocols sync from notify view POST") tasks.iana_protocols_update_task.delay() - elif notification == "queue": - log("Running sync script from notify view POST") - runscript("rfc-editor-queue-updates") return HttpResponse("OK", content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET) diff --git a/ietf/templates/base.html b/ietf/templates/base.html index ccecd8eb1c..f426d361ce 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -140,6 +140,7 @@ {% endif %} {% endif %} +
System Status Report a bug: diff --git a/ietf/templates/base/menu.html b/ietf/templates/base/menu.html index 5a0ba2ba5d..1d7bad8724 100644 --- a/ietf/templates/base/menu.html +++ b/ietf/templates/base/menu.html @@ -440,6 +440,12 @@ Release notes +
  • + + System status + +
  • {% if flavor == 'top' %}
  • diff --git a/ietf/templates/doc/ballot/writeupnotes.html b/ietf/templates/doc/ballot/writeupnotes.html index 925387d28d..8e985c15c7 100644 --- a/ietf/templates/doc/ballot/writeupnotes.html +++ b/ietf/templates/doc/ballot/writeupnotes.html @@ -15,11 +15,16 @@

    {% bootstrap_form ballot_writeup_form %}
    Technical summary, Working Group summary, document quality, personnel, IANA note. This text will be appended to all announcements and messages to the IRTF or RFC Editor. - {% if ballot_issue_danger %} + {% if warn_lc %}

    This document has not completed IETF Last Call. Please do not issue the ballot early without good reason.

    {% endif %} + {% if warn_unexpected_state %} +

    + This document is in an IESG state of "{{warn_unexpected_state}}". It would be unexpected to issue a ballot while in this state. +

    + {% endif %}
    {% endif %} - {% if user|has_role:"Secretariat" %} - - Send Email - - {% endif %} {% if show_send_full_url %}

    @@ -584,39 +576,10 @@

    {% endif %} - {% if e.desc|startswith:"Received message" or e.desc|startswith:"Sent message" %} - {% with m=e.submissionemailevent.message %} - {% if user.is_authenticated %} - - {% if e.desc|startswith:"Received message" and user|has_role:"Secretariat" %} - - Reply - - {% endif %} - Email: - - {{ e.desc }} - - - {% else %} - - Email: - - {{ e.desc }} - - - {% endif %} - {% endwith %} - {% else %} - - {{ e.desc|urlize_ietf_docs|linkify }} - - {% endif %} + + + {{ e.desc|urlize_ietf_docs|linkify }} + {% endfor %} diff --git a/ietf/templates/submit/submitter_form.html b/ietf/templates/submit/submitter_form.html index 1cf77260ee..65b9ba5094 100644 --- a/ietf/templates/submit/submitter_form.html +++ b/ietf/templates/submit/submitter_form.html @@ -11,12 +11,14 @@

    Submitter

    {% load ietf_filters %} {% for author in submission.authors %} - + {% if author.name %} + + {% endif %} {% endfor %} {% bootstrap_form_errors submitter_form %} {% bootstrap_field submitter_form.name %} diff --git a/ietf/utils/__init__.py b/ietf/utils/__init__.py index 7f1df97602..fbe55eb043 100644 --- a/ietf/utils/__init__.py +++ b/ietf/utils/__init__.py @@ -1 +1,29 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# Copyright The IETF Trust 2007-2024, All Rights Reserved +import subprocess + + +class _ToolVersionManager: + _known = [ + "pyang", + "xml2rfc", + "xym", + "yanglint", + ] + _versions: dict[str, str] = dict() + + def __getitem__(self, item): + if item not in self._known: + return "Unknown" + elif item not in self._versions: + try: + self._versions[item] = subprocess.run( + [item, "--version"], + capture_output=True, + check=True, + ).stdout.decode().strip() + except subprocess.CalledProcessError: + return "Unknown" + return self._versions[item] + + +tool_version = _ToolVersionManager() diff --git a/ietf/utils/admin.py b/ietf/utils/admin.py index fa1ebb7081..6c1c8726e1 100644 --- a/ietf/utils/admin.py +++ b/ietf/utils/admin.py @@ -5,8 +5,6 @@ from django.contrib import admin from django.utils.encoding import force_str -from ietf.utils.models import VersionInfo - def name(obj): if hasattr(obj, 'abbrev'): return obj.abbrev() @@ -58,8 +56,3 @@ class DumpInfoAdmin(admin.ModelAdmin): list_display = ['date', 'host', 'tz'] list_filter = ['date'] admin.site.register(DumpInfo, DumpInfoAdmin) - -class VersionInfoAdmin(admin.ModelAdmin): - list_display = ['command', 'switch', 'version', 'time', ] -admin.site.register(VersionInfo, VersionInfoAdmin) - diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index 7c8e3fbc5d..56c28c4b19 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -4,7 +4,6 @@ import datetime -from decorator import decorator, decorate from functools import wraps from django.conf import settings @@ -20,25 +19,29 @@ from ietf.person.models import Person, PersonalApiKey, PersonApiKeyEvent from ietf.utils import log -@decorator -def skip_coverage(f, *args, **kwargs): - if settings.TEST_CODE_COVERAGE_CHECKER: - set_coverage_checking(False) - result = f(*args, **kwargs) - set_coverage_checking(True) - return result - else: - return f(*args, **kwargs) - -@decorator -def person_required(f, request, *args, **kwargs): - if not request.user.is_authenticated: - raise ValueError("The @person_required decorator should be called after @login_required.") - try: - request.user.person - except Person.DoesNotExist: - return render(request, 'registration/missing_person.html') - return f(request, *args, **kwargs) +def skip_coverage(f): + @wraps(f) + def _wrapper(*args, **kwargs): + if settings.TEST_CODE_COVERAGE_CHECKER: + set_coverage_checking(False) + result = f(*args, **kwargs) + set_coverage_checking(True) + return result + else: + return f(*args, **kwargs) + return _wrapper + +def person_required(f): + @wraps(f) + def _wrapper(request, *args, **kwargs): + if not request.user.is_authenticated: + raise ValueError("The @person_required decorator should be called after @login_required.") + try: + request.user.person + except Person.DoesNotExist: + return render(request, 'registration/missing_person.html') + return f(request, *args, **kwargs) + return _wrapper def require_api_key(f): @@ -90,29 +93,31 @@ def err(code, text): return _wrapper -def _memoize(func, self, *args, **kwargs): - '''Memoize wrapper for instance methods. Use @lru_cache for functions.''' - if kwargs: # frozenset is used to ensure hashability - key = args, frozenset(list(kwargs.items())) - else: - key = args - # instance method, set up cache if needed - if not hasattr(self, '_cache'): - self._cache = {} - if not func in self._cache: - self._cache[func] = {} - # - cache = self._cache[func] - if key not in cache: - cache[key] = func(self, *args, **kwargs) - return cache[key] def memoize(func): + @wraps(func) + def _memoize(self, *args, **kwargs): + '''Memoize wrapper for instance methods. Use @lru_cache for functions.''' + if kwargs: # frozenset is used to ensure hashability + key = args, frozenset(list(kwargs.items())) + else: + key = args + # instance method, set up cache if needed + if not hasattr(self, '_cache'): + self._cache = {} + if not func in self._cache: + self._cache[func] = {} + # + cache = self._cache[func] + if key not in cache: + cache[key] = func(self, *args, **kwargs) + return cache[key] + if not hasattr(func, '__class__'): raise NotImplementedError("Use @lru_cache instead of memoize() for functions.") # For methods, we want the cache on the object, not on the class, in order # to not having to think about cache bloat and content becoming stale, so # we cannot set up the cache here. - return decorate(func, _memoize) + return _memoize def ignore_view_kwargs(*args): diff --git a/ietf/utils/fields.py b/ietf/utils/fields.py index 95d8a2aa7e..3e6f56d45e 100644 --- a/ietf/utils/fields.py +++ b/ietf/utils/fields.py @@ -14,7 +14,7 @@ from django import forms from django.db import models # pyflakes:ignore -from django.core.validators import validate_email +from django.core.validators import ProhibitNullCharactersValidator, validate_email from django.core.exceptions import ValidationError from django.utils.dateparse import parse_duration @@ -353,3 +353,20 @@ def update_dimension_fields(self, *args, **kwargs): super().update_dimension_fields(*args, **kwargs) except FileNotFoundError: pass # don't do anything if the file has gone missing + + +class ModelMultipleChoiceField(forms.ModelMultipleChoiceField): + """ModelMultipleChoiceField that rejects null characters cleanly""" + validate_no_nulls = ProhibitNullCharactersValidator() + + def clean(self, value): + try: + for item in value: + self.validate_no_nulls(item) + except TypeError: + # A TypeError probably means value is not iterable, which most commonly comes up + # with None as a value. If it's something more exotic, we don't know how to test + # for null characters anyway. Either way, trust the superclass clean() method to + # handle it. + pass + return super().clean(value) diff --git a/ietf/utils/jsonlogger.py b/ietf/utils/jsonlogger.py new file mode 100644 index 0000000000..a9eeb02ba9 --- /dev/null +++ b/ietf/utils/jsonlogger.py @@ -0,0 +1,26 @@ +# Copyright The IETF Trust 2024, All Rights Reserved +from pythonjsonlogger import jsonlogger +import time + + +class DatatrackerJsonFormatter(jsonlogger.JsonFormatter): + converter = time.gmtime # use UTC + default_msec_format = "%s.%03d" # '.' instead of ',' + + +class GunicornRequestJsonFormatter(DatatrackerJsonFormatter): + """Only works with Gunicorn's logging""" + def add_fields(self, log_record, record, message_dict): + super().add_fields(log_record, record, message_dict) + log_record.setdefault("method", record.args["m"]) + log_record.setdefault("proto", record.args["H"]) + log_record.setdefault("remote_ip", record.args["h"]) + path = record.args["U"] # URL path + if record.args["q"]: # URL query string + path = "?".join([path, record.args["q"]]) + log_record.setdefault("path", path) + log_record.setdefault("status", record.args["s"]) + log_record.setdefault("referer", record.args["f"]) + log_record.setdefault("user_agent", record.args["a"]) + log_record.setdefault("len_bytes", record.args["B"]) + log_record.setdefault("duration_ms", record.args["M"]) diff --git a/ietf/utils/log.py b/ietf/utils/log.py index d5a54e5516..2a068ade9a 100644 --- a/ietf/utils/log.py +++ b/ietf/utils/log.py @@ -9,37 +9,10 @@ import os.path import traceback -from typing import Callable # pyflakes:ignore - -try: - import syslog - logfunc = syslog.syslog # type: Callable -except ImportError: # import syslog will fail on Windows boxes - logging.basicConfig(filename='tracker.log',level=logging.INFO) - logfunc = logging.info - pass - from django.conf import settings import debug # pyflakes:ignore -formatter = logging.Formatter('{levelname}: {name}:{lineno}: {message}', style='{') -for name, level in settings.UTILS_LOGGER_LEVELS.items(): - logger = logging.getLogger(name) - if not logger.hasHandlers(): - debug.say(' Adding handlers to logger %s' % logger.name) - - handlers = [ - logging.StreamHandler(), - logging.handlers.SysLogHandler(address='/dev/log', - facility=logging.handlers.SysLogHandler.LOG_USER), - ] - for h in handlers: - h.setFormatter(formatter) - h.setLevel(level) - logger.addHandler(h) - debug.say(" Setting %s logging level to %s" % (logger.name, level)) - logger.setLevel(level) def getclass(frame): cls = None @@ -56,20 +29,9 @@ def getcaller(): return (pmodule, pclass, pfunction, pfile, pline) def log(msg, e=None): - "Uses syslog by preference. Logs the given calling point and message." - global logfunc - def _flushfunc(): - pass - _logfunc = logfunc - if settings.SERVER_MODE == 'test': - if getattr(settings, 'show_logging', False) is True: - _logfunc = debug.say - _flushfunc = sys.stdout.flush # pyflakes:ignore (intentional redefinition) - else: + "Logs the given calling point and message to the logging framework's datatracker handler at severity INFO" + if settings.SERVER_MODE == 'test' and not getattr(settings, 'show_logging',False): return - elif settings.DEBUG == True: - _logfunc = debug.say - _flushfunc = sys.stdout.flush # pyflakes:ignore (intentional redefinition) if not isinstance(msg, str): msg = msg.encode('unicode_escape') try: @@ -82,11 +44,8 @@ def _flushfunc(): where = " in " + func + "()" except IndexError: file, line, where = "/", 0, "" - _flushfunc() - _logfunc("ietf%s(%d)%s: %s" % (file, line, where, msg)) - -logger = logging.getLogger('django') + logging.getLogger("datatracker").info(msg=msg, extra = {"file":file, "line":line, "where":where}) def exc_parts(): @@ -124,6 +83,7 @@ def assertion(statement, state=True, note=None): This acts like an assertion. It uses the django logger in order to send the failed assertion and a backtrace as for an internal server error. """ + logger = logging.getLogger("django") # Note this is a change - before this would have gone to "django" frame = inspect.currentframe().f_back value = eval(statement, frame.f_globals, frame.f_locals) if bool(value) != bool(state): @@ -148,6 +108,7 @@ def assertion(statement, state=True, note=None): def unreachable(date="(unknown)"): "Raises an assertion or sends traceback to admins if executed." + logger = logging.getLogger("django") frame = inspect.currentframe().f_back if settings.DEBUG is True or settings.SERVER_MODE == 'test': raise AssertionError("Arrived at code in %s() which was marked unreachable on %s." % (frame.f_code.co_name, date)) diff --git a/ietf/utils/management/commands/import_htpasswd.py b/ietf/utils/management/commands/import_htpasswd.py deleted file mode 100644 index c33a46b727..0000000000 --- a/ietf/utils/management/commands/import_htpasswd.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright The IETF Trust 2014-2020, All Rights Reserved -import io -import sys - -from textwrap import dedent - -from django.contrib.auth.models import User -from django.core.management.base import BaseCommand - -def import_htpasswd_file(filename, verbosity=1, overwrite=False): - with io.open(filename) as file: - for line in file: - if not ':' in line: - raise ValueError('Found a line without colon separator in the htpassword file %s:' - ' "%s"' % (file.name, line)) - username, password = line.strip().split(':', 1) - try: - user = User.objects.get(username__iexact=username) - if overwrite == True or not user.password: - if password.startswith('{SHA}'): - user.password = "sha1$$%s" % password[len('{SHA}'):] - elif password.startswith('$apr1$'): - user.password = "md5$%s" % password[len('$apr1$'):] - else: # Assume crypt - user.password = "crypt$$%s" % password - user.save() - if verbosity > 0: - sys.stderr.write('.') - if verbosity > 1: - sys.stderr.write(' %s\n' % username) - except User.DoesNotExist: - if verbosity > 1: - sys.stderr.write('\nNo such user: %s\n' % username) - -class Command(BaseCommand): - """ - Import passwords from one or more htpasswd files to Django's auth_user table. - - This command only imports passwords; it does not import usernames, as that - would leave usernames without associated Person records in the database, - something which is undesirable. - - By default the command won't overwrite existing password entries, but - given the --force switch, it will overwrite existing entries too. Without - the --force switch, the command is safe to run repeatedly. - """ - - help = dedent(__doc__).strip() - - def add_arguments(self, parser): - parser.add_argument('--force', - action='store_true', dest='overwrite', default=False, - help='Overwrite existing passwords in the auth_user table.') - - - args = '[path [path [...]]]' - - def handle(self, *filenames, **options): - overwrite = options.get('overwrite', False) - verbosity = int(options.get('verbosity')) - for fn in filenames: - import_htpasswd_file(fn, verbosity=verbosity, overwrite=overwrite) - diff --git a/ietf/utils/management/commands/patch_libraries.py b/ietf/utils/management/commands/patch_libraries.py new file mode 100644 index 0000000000..d9ae11097b --- /dev/null +++ b/ietf/utils/management/commands/patch_libraries.py @@ -0,0 +1,31 @@ +# Copyright The IETF Trust 2024, All Rights Reserved +import django + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from pathlib import Path + +from ietf.utils import patch + + +class Command(BaseCommand): + """Apply IETF patches to libraries""" + requires_system_checks = tuple() + + def handle(self, *args, **options): + library_path = Path(django.__file__).parent.parent + top_dir = Path(settings.BASE_DIR).parent + + # All patches in settings.CHECKS_LIBRARY_PATCHES_TO_APPLY must have a + # relative file path starting from the site-packages dir, e.g. + # 'django/db/models/fields/__init__.py' + for patch_file in settings.CHECKS_LIBRARY_PATCHES_TO_APPLY: + patch_set = patch.fromfile(top_dir / Path(patch_file)) + if not patch_set: + raise CommandError(f"Could not parse patch file '{patch_file}'") + if not patch_set.apply(root=bytes(library_path)): + raise CommandError(f"Could not apply the patch from '{patch_file}'") + if patch_set.already_patched: + self.stdout.write(f"Patch from '{patch_file}' was already applied") + else: + self.stdout.write(f"Applied the patch from '{patch_file}'") diff --git a/ietf/utils/management/commands/periodic_tasks.py b/ietf/utils/management/commands/periodic_tasks.py index e359382839..2d34f8361c 100644 --- a/ietf/utils/management/commands/periodic_tasks.py +++ b/ietf/utils/management/commands/periodic_tasks.py @@ -141,6 +141,16 @@ def create_default_tasks(self): ), ) + PeriodicTask.objects.get_or_create( + name="Expire Last Calls", + task="ietf.doc.tasks.expire_last_calls_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["daily"], + description="Move docs whose last call has expired to their next states", + ), + ) + PeriodicTask.objects.get_or_create( name="Sync with IANA changes", task="ietf.sync.tasks.iana_changes_update_task", @@ -181,6 +191,98 @@ def create_default_tasks(self): ) ) + PeriodicTask.objects.get_or_create( + name="Generate idnits2 rfcs-obsoleted blob", + task="ietf.doc.tasks.generate_idnits2_rfcs_obsoleted_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["hourly"], + description="Generate the rfcs-obsoleted file used by idnits", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Generate idnits2 rfc-status blob", + task="ietf.doc.tasks.generate_idnits2_rfc_status_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["hourly"], + description="Generate the rfc_status blob used by idnits", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Send NomCom reminders", + task="ietf.nomcom.tasks.send_nomcom_reminders_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["daily"], + description="Send acceptance and questionnaire reminders to nominees", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Generate WG charter files", + task="ietf.group.tasks.generate_wg_charters_files_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["hourly"], + description="Update 1wg-charters.txt and 1wg-charters-by-acronym.txt", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Generate WG summary files", + task="ietf.group.tasks.generate_wg_summary_files_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["hourly"], + description="Update 1wg-summary.txt and 1wg-summary-by-acronym.txt", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Generate I-D bibxml files", + task="ietf.doc.tasks.generate_draft_bibxml_files_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["hourly"], + description="Generate draft bibxml files for the last week's drafts", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Send personal API key usage emails", + task="ietf.person.tasks.send_apikey_usage_emails_task", + kwargs=json.dumps(dict(days=7)), + defaults=dict( + enabled=False, + crontab=self.crontabs["weekly"], + description="Send personal API key usage summary emails for the past week", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Purge old personal API key events", + task="ietf.person.tasks.purge_personal_api_key_events_task", + kwargs=json.dumps(dict(keep_days=14)), + defaults=dict( + enabled=False, + crontab=self.crontabs["daily"], + description="Purge PersonApiKeyEvent instances older than 14 days", + ), + ) + + PeriodicTask.objects.get_or_create( + name="Run Yang model checks", + task="ietf.submit.tasks.run_yang_model_checks_task", + defaults=dict( + enabled=False, + crontab=self.crontabs["daily"], + description="Re-run Yang model checks on all active drafts", + ), + ) + def show_tasks(self): for label, crontab in self.crontabs.items(): tasks = PeriodicTask.objects.filter(crontab=crontab).order_by( diff --git a/ietf/utils/management/commands/populate_yang_model_dirs.py b/ietf/utils/management/commands/populate_yang_model_dirs.py deleted file mode 100644 index 864dfafb72..0000000000 --- a/ietf/utils/management/commands/populate_yang_model_dirs.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright The IETF Trust 2016-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import io -import os -import sys -import time - -from pathlib import Path -from textwrap import dedent -from xym import xym - -from django.conf import settings -from django.core.management.base import BaseCommand - -import debug # pyflakes:ignore - -class Command(BaseCommand): - """ - Populate the yang module repositories from drafts and RFCs. - - Extracts yang models from RFCs (found in settings.RFC_PATH and places - them in settings.SUBMIT_YANG_RFC_MODEL_DIR, and from active drafts, placed in - settings.SUBMIT_YANG_DRAFT_MODEL_DIR. - - """ - - help = dedent(__doc__).strip() - - def add_arguments(self, parser): - parser.add_argument('--clean', - action='store_true', dest='clean', default=False, - help='Remove the current directory content before writing new models.') - - - def handle(self, *filenames, **options): - """ - - * All yang modules from published RFCs should be extracted and be - available in an rfc-yang repository. - - * All valid yang modules from active, not replaced, Internet-Drafts - should be extracted and be available in a draft-valid-yang repository. - - * All, valid and invalid, yang modules from active, not replaced, - Internet-Drafts should be available in a draft-all-yang repository. - (Actually, given precedence ordering, it would be enough to place - non-validating modules in a draft-invalid-yang repository instead). - - * In all cases, example modules should be excluded. - - * Precedence is established by the search order of the repository as - provided to pyang. - - * As drafts expire, models should be removed in order to catch cases - where a module being worked on depends on one which has slipped out - of the work queue. - - """ - - verbosity = int(options.get('verbosity')) - - def extract_from(file, dir, strict=True): - saved_stdout = sys.stdout - saved_stderr = sys.stderr - xymerr = io.StringIO() - xymout = io.StringIO() - sys.stderr = xymerr - sys.stdout = xymout - model_list = [] - try: - model_list = xym.xym(str(file), str(file.parent), str(dir), strict=strict, debug_level=verbosity-2) - for name in model_list: - modfile = moddir / name - mtime = file.stat().st_mtime - os.utime(str(modfile), (mtime, mtime)) - if '"' in name: - name = name.replace('"', '') - modfile.rename(str(moddir/name)) - model_list = [ n.replace('"','') for n in model_list ] - except Exception as e: - self.stderr.write("** Error when extracting from %s: %s" % (file, str(e))) - finally: - sys.stdout = saved_stdout - sys.stderr = saved_stderr - # - if verbosity > 1: - outmsg = xymout.getvalue() - if outmsg.strip(): - self.stdout.write(outmsg) - if verbosity>2: - errmsg = xymerr.getvalue() - if errmsg.strip(): - self.stderr.write(errmsg) - return model_list - - # Extract from new RFCs - - rfcdir = Path(settings.RFC_PATH) - - moddir = Path(settings.SUBMIT_YANG_RFC_MODEL_DIR) - if not moddir.exists(): - moddir.mkdir(parents=True) - - latest = 0 - for item in moddir.iterdir(): - if item.stat().st_mtime > latest: - latest = item.stat().st_mtime - - if verbosity > 0: - self.stdout.write("Extracting to %s ..." % moddir) - for item in rfcdir.iterdir(): - if item.is_file() and item.name.startswith('rfc') and item.name.endswith('.txt') and item.name[3:-4].isdigit(): - if item.stat().st_mtime > latest: - model_list = extract_from(item, moddir) - for name in model_list: - if name.startswith('ietf') or name.startswith('iana'): - if verbosity > 1: - self.stdout.write(" Extracted from %s: %s" % (item, name)) - elif verbosity > 0: - self.stdout.write('.', ending='') - self.stdout.flush() - else: - modfile = moddir / name - modfile.unlink() - if verbosity > 1: - self.stdout.write(" Skipped module from %s: %s" % (item, name)) - if verbosity > 0: - self.stdout.write("") - - # Extract valid modules from drafts - - six_months_ago = time.time() - 6*31*24*60*60 - def active(item): - return item.stat().st_mtime > six_months_ago - - draftdir = Path(settings.INTERNET_DRAFT_PATH) - - moddir = Path(settings.SUBMIT_YANG_DRAFT_MODEL_DIR) - if not moddir.exists(): - moddir.mkdir(parents=True) - if verbosity > 0: - self.stdout.write("Emptying %s ..." % moddir) - for item in moddir.iterdir(): - item.unlink() - - if verbosity > 0: - self.stdout.write("Extracting to %s ..." % moddir) - for item in draftdir.iterdir(): - try: - if item.is_file() and item.name.startswith('draft') and item.name.endswith('.txt') and active(item): - model_list = extract_from(item, moddir, strict=False) - for name in model_list: - if not name.startswith('example'): - if verbosity > 1: - self.stdout.write(" Extracted module from %s: %s" % (item, name)) - elif verbosity > 0: - self.stdout.write('.', ending='') - self.stdout.flush() - else: - modfile = moddir / name - modfile.unlink() - if verbosity > 1: - self.stdout.write(" Skipped module from %s: %s" % (item, name)) - except UnicodeDecodeError as e: - self.stderr.write('\nError: %s' % (e, )) - self.stderr.write(item.name) - self.stderr.write('') - if verbosity > 0: - self.stdout.write('') - diff --git a/ietf/utils/management/commands/run_yang_model_checks.py b/ietf/utils/management/commands/run_yang_model_checks.py deleted file mode 100644 index 7e2f7165b0..0000000000 --- a/ietf/utils/management/commands/run_yang_model_checks.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright The IETF Trust 2017-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import json - -from textwrap import dedent - -from django.core.management.base import BaseCommand - -import debug # pyflakes:ignore - -from ietf.doc.models import Document, State -from ietf.submit.models import Submission -from ietf.submit.checkers import DraftYangChecker - - -class Command(BaseCommand): - """ - Run yang model checks on active drafts. - - Repeats the yang checks in ietf/submit/checkers.py for active drafts, in - order to catch changes in status due to new modules becoming available in - the module directories. - - """ - - help = dedent(__doc__).strip() - - def add_arguments(self, parser): - parser.add_argument('draftnames', nargs="*", help="drafts to check, or none to check all active yang drafts") - parser.add_argument('--clean', - action='store_true', dest='clean', default=False, - help='Remove the current directory content before writing new models.') - - - def check_yang(self, checker, draft, force=False): - if self.verbosity > 1: - self.stdout.write("Checking %s-%s" % (draft.name, draft.rev)) - elif self.verbosity > 0: - self.stderr.write('.', ending='') - submission = Submission.objects.filter(name=draft.name, rev=draft.rev).order_by('-id').first() - if submission or force: - check = submission.checks.filter(checker=checker.name).order_by('-id').first() - if check or force: - result = checker.check_file_txt(draft.get_file_name()) - passed, message, errors, warnings, items = result - if self.verbosity > 2: - self.stdout.write(" Errors: %s\n" - " Warnings: %s\n" - " Message:\n%s\n" % (errors, warnings, message)) - items = json.loads(json.dumps(items)) - new_res = (passed, errors, warnings, message) - old_res = (check.passed, check.errors, check.warnings, check.message) if check else () - if new_res != old_res: - if self.verbosity > 1: - self.stdout.write(" Saving new yang checker results for %s-%s" % (draft.name, draft.rev)) - qs = submission.checks.filter(checker=checker.name).order_by('time') - submission.checks.filter(checker=checker.name).exclude(pk=qs.first().pk).delete() - submission.checks.create(submission=submission, checker=checker.name, passed=passed, - message=message, errors=errors, warnings=warnings, items=items, - symbol=checker.symbol) - else: - self.stderr.write("Error: did not find any submission object for %s-%s" % (draft.name, draft.rev)) - - def handle(self, *filenames, **options): - """ - """ - - self.verbosity = int(options.get('verbosity')) - drafts = options.get('draftnames') - - active_state = State.objects.get(type="draft", slug="active") - - checker = DraftYangChecker() - if drafts: - for name in drafts: - parts = name.rsplit('-',1) - if len(parts)==2 and len(parts[1])==2 and parts[1].isdigit(): - name = parts[0] - draft = Document.objects.get(name=name) - self.check_yang(checker, draft, force=True) - else: - for draft in Document.objects.filter(states=active_state, type_id='draft'): - self.check_yang(checker, draft) diff --git a/ietf/utils/management/commands/showloggers.py b/ietf/utils/management/commands/showloggers.py index 3de9db0c06..b79da9ce26 100644 --- a/ietf/utils/management/commands/showloggers.py +++ b/ietf/utils/management/commands/showloggers.py @@ -11,18 +11,7 @@ import debug # pyflakes:ignore class Command(BaseCommand): - """ - Display a list or tree representation of python loggers. - - Add a UTILS_LOGGER_LEVELS setting in settings_local.py to configure - non-default logging levels for any registered logger, for instance: - - UTILS_LOGGER_LEVELS = { - 'oicd_provider': 'DEBUG', - 'urllib3.connection': 'DEBUG', - } - - """ + """Display a list or tree representation of python loggers""" help = dedent(__doc__).strip() diff --git a/ietf/utils/management/commands/update_external_command_info.py b/ietf/utils/management/commands/update_external_command_info.py deleted file mode 100644 index e9e24f000d..0000000000 --- a/ietf/utils/management/commands/update_external_command_info.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright The IETF Trust 2017-2020, All Rights Reserved -# -*- coding: utf-8 -*- - - -import sys - -from textwrap import dedent - -from django.core.management.base import BaseCommand - -import debug # pyflakes:ignore - -from ietf.utils.models import VersionInfo -from ietf.utils.pipe import pipe - -class Command(BaseCommand): - """ - Update the version information for external commands used by the datatracker. - - Iterates through the entries in the VersionInfo table, runs the relevant - command, and updates the version string with the result. - - """ - - help = dedent(__doc__).strip() - - def handle(self, *filenames, **options): - for c in VersionInfo.objects.filter(used=True): - cmd = "%s %s" % (c.command, c.switch) - code, out, err = pipe(cmd) - out = out.decode('utf-8') - err = err.decode('utf-8') - if code != 0: - sys.stderr.write("Command '%s' returned %s: \n%s\n%s\n" % (cmd, code, out, err)) - else: - c.version = (out.strip()+'\n'+err.strip()).strip() - if options.get('verbosity', 1) > 1: - sys.stdout.write( - "Command: %s\n" - " Version: %s\n" % (cmd, c.version)) - c.save() diff --git a/ietf/utils/meetecho.py b/ietf/utils/meetecho.py index e842ca0121..2f5f146766 100644 --- a/ietf/utils/meetecho.py +++ b/ietf/utils/meetecho.py @@ -115,6 +115,7 @@ def retrieve_wg_tokens(self, acronyms: Union[str, Sequence[str]]): def schedule_meeting( self, wg_token: str, + room_id: int, description: str, start_time: datetime.datetime, duration: datetime.timedelta, @@ -139,6 +140,7 @@ def schedule_meeting( } :param wg_token: token retrieved via retrieve_wg_tokens() + :param room_id: int id to identify the room (will be echoed as room.id) :param description: str describing the meeting :param start_time: starting time as a datetime :param duration: duration as a timedelta @@ -151,6 +153,7 @@ def schedule_meeting( "meeting/interim/createRoom", api_token=wg_token, json={ + "room_id": room_id, "description": description, "start_time": self._serialize_time(start_time), "duration": self._serialize_duration(duration), @@ -455,9 +458,10 @@ def fetch(self, group): response = self.api.fetch_meetings(self.wg_token(group)) return Conference.from_api_dict(self, response["rooms"]) - def create(self, group, description, start_time, duration, extrainfo=""): + def create(self, group, session_id, description, start_time, duration, extrainfo=""): response = self.api.schedule_meeting( wg_token=self.wg_token(group), + room_id=int(session_id), description=description, start_time=start_time, duration=duration, diff --git a/ietf/utils/migrations/0002_delete_versioninfo.py b/ietf/utils/migrations/0002_delete_versioninfo.py new file mode 100644 index 0000000000..2835bb017b --- /dev/null +++ b/ietf/utils/migrations/0002_delete_versioninfo.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.11 on 2024-05-03 21:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("utils", "0001_initial"), + ] + + operations = [ + migrations.DeleteModel( + name="VersionInfo", + ), + ] diff --git a/ietf/utils/models.py b/ietf/utils/models.py index 0915537fd8..21af5766e9 100644 --- a/ietf/utils/models.py +++ b/ietf/utils/models.py @@ -9,15 +9,6 @@ class DumpInfo(models.Model): host = models.CharField(max_length=128) tz = models.CharField(max_length=32, default='UTC') -class VersionInfo(models.Model): - time = models.DateTimeField(auto_now=True) - command = models.CharField(max_length=32) - switch = models.CharField(max_length=16) - version = models.CharField(max_length=64) - used = models.BooleanField(default=True) - class Meta: - verbose_name_plural = 'VersionInfo' - class ForeignKey(models.ForeignKey): "A local ForeignKey proxy which provides the on_delete value required under Django 2.0." def __init__(self, to, on_delete=models.CASCADE, **kwargs): diff --git a/ietf/utils/resources.py b/ietf/utils/resources.py index 6d61c5e2ed..1252cfef14 100644 --- a/ietf/utils/resources.py +++ b/ietf/utils/resources.py @@ -12,7 +12,7 @@ from django.contrib.contenttypes.models import ContentType from ietf import api -from ietf.utils.models import DumpInfo, VersionInfo +from ietf.utils.models import DumpInfo class UserResource(ModelResource): @@ -43,21 +43,3 @@ class Meta: "host": ALL, } api.utils.register(DumpInfoResource()) - - -class VersionInfoResource(ModelResource): - class Meta: - queryset = VersionInfo.objects.all() - serializer = api.Serializer() - cache = SimpleCache() - #resource_name = 'versioninfo' - ordering = ['id', ] - filtering = { - "id": ALL, - "time": ALL, - "command": ALL, - "switch": ALL, - "version": ALL, - "used": ALL, - } -api.utils.register(VersionInfoResource()) diff --git a/ietf/utils/test_utils.py b/ietf/utils/test_utils.py index ddd274a613..ba35665a8d 100644 --- a/ietf/utils/test_utils.py +++ b/ietf/utils/test_utils.py @@ -211,6 +211,7 @@ class TestCase(django.test.TestCase): 'INTERNET_DRAFT_ARCHIVE_DIR', 'INTERNET_DRAFT_PATH', 'BIBXML_BASE_PATH', + 'FTP_DIR', ] parser = html5lib.HTMLParser(strict=True) diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index 4ac2732ed8..08adefc826 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -486,6 +486,51 @@ def test_parse_docname(self): ("-01", None), ) + def test_render_author_name(self): + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element("author", fullname="Joanna Q. Public")), + "Joanna Q. Public", + ) + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element( + "author", + fullname="Joanna Q. Public", + asciiFullname="Not the Same at All", + )), + "Joanna Q. Public", + ) + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element( + "author", + fullname="Joanna Q. Public", + initials="J. Q.", + surname="Public-Private", + )), + "Joanna Q. Public", + ) + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element( + "author", + initials="J. Q.", + surname="Public", + )), + "J. Q. Public", + ) + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element( + "author", + surname="Public", + )), + "Public", + ) + self.assertEqual( + XMLDraft.render_author_name(lxml.etree.Element( + "author", + initials="J. Q.", + )), + "J. Q.", + ) + class NameTests(TestCase): diff --git a/ietf/utils/tests_meetecho.py b/ietf/utils/tests_meetecho.py index 39f36969b4..1aef5894e2 100644 --- a/ietf/utils/tests_meetecho.py +++ b/ietf/utils/tests_meetecho.py @@ -82,7 +82,7 @@ def test_schedule_meeting(self): 'rooms': { '3d55bce0-535e-4ba8-bb8e-734911cf3c32': { 'room': { - 'id': 18, + 'id': 18, # should match room_id in api.schedule_meeting() below 'start_time': '2021-09-14 10:00:00', 'duration': 130, 'description': 'interim-2021-wgname-01', @@ -97,6 +97,7 @@ def test_schedule_meeting(self): api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET) api_response = api.schedule_meeting( wg_token='my-token', + room_id=18, start_time=datetime.datetime(2021, 9, 14, 10, 0, 0, tzinfo=datetime.timezone.utc), duration=datetime.timedelta(minutes=130), description='interim-2021-wgname-01', @@ -116,6 +117,7 @@ def test_schedule_meeting(self): self.assertEqual( request.json(), { + 'room_id': 18, 'duration': 130, 'start_time': '2021-09-14 10:00:00', 'extrainfo': 'message for staff', @@ -485,7 +487,7 @@ def test_create(self, mock_schedule, _): 'rooms': { 'session-1-uuid': { 'room': { - 'id': 1, + 'id': 1, # value should match session_id param to cm.create() below 'start_time': datetime.datetime(2022,2,4,1,2,3, tzinfo=datetime.timezone.utc), 'duration': datetime.timedelta(minutes=45), 'description': 'some-description', @@ -496,7 +498,7 @@ def test_create(self, mock_schedule, _): }, } cm = ConferenceManager(settings.MEETECHO_API_CONFIG) - result = cm.create('group', 'desc', 'starttime', 'dur', 'extra') + result = cm.create('group', '1', 'desc', 'starttime', 'dur', 'extra') self.assertEqual( result, [Conference( @@ -515,6 +517,7 @@ def test_create(self, mock_schedule, _): kwargs, { 'wg_token': 'atoken', + 'room_id': 1, 'description': 'desc', 'start_time': 'starttime', 'duration': 'dur', diff --git a/ietf/utils/validators.py b/ietf/utils/validators.py index 9642a2877c..8fe989df99 100644 --- a/ietf/utils/validators.py +++ b/ietf/utils/validators.py @@ -60,6 +60,7 @@ def __ne__(self, other): validate_regular_expression_string = RegexStringValidator() + def validate_file_size(file, missing_ok=False): try: size = file.size @@ -69,8 +70,14 @@ def validate_file_size(file, missing_ok=False): else: raise - if size > settings.SECR_MAX_UPLOAD_SIZE: - raise ValidationError('Please keep filesize under %s. Requested upload size was %s' % (filesizeformat(settings.SECR_MAX_UPLOAD_SIZE), filesizeformat(file.size))) + if size > settings.DATATRACKER_MAX_UPLOAD_SIZE: + raise ValidationError( + "Please keep filesize under {}. Requested upload size was {}".format( + filesizeformat(settings.DATATRACKER_MAX_UPLOAD_SIZE), + filesizeformat(file.size) + ) + ) + def validate_mime_type(file, valid, missing_ok=False): try: diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py index 3a9ac02b1d..c39c4d0a06 100644 --- a/ietf/utils/xmldraft.py +++ b/ietf/utils/xmldraft.py @@ -179,6 +179,29 @@ def get_creation_date(self): # abstract = self.xmlroot.findtext('front/abstract') # return abstract.strip() if abstract else '' + @staticmethod + def render_author_name(author_elt): + """Get a displayable name for an author, if possible + + Based on TextWriter.render_author_name() from xml2rfc. If fullname is present, uses that. + If not, uses either initials + surname or just surname. Finally, returns None because this + author is evidently an organization, not a person. + + Does not involve ascii* attributes because rfc7991 requires fullname if any of those are + present. + """ + # Use fullname attribute, if present + fullname = author_elt.attrib.get("fullname", "").strip() + if fullname: + return fullname + surname = author_elt.attrib.get("surname", "").strip() + initials = author_elt.attrib.get("initials", "").strip() + if surname or initials: + # This allows the possibility that only initials are used, which is a bit nonsensical + # but seems to be technically allowed by RFC 7991. + return f"{initials} {surname}".strip() + return None + def get_author_list(self): """Get detailed author list @@ -197,7 +220,7 @@ def get_author_list(self): for author in self.xmlroot.findall('front/author'): info = { - 'name': author.attrib.get('fullname'), + 'name': self.render_author_name(author), 'email': author.findtext('address/email'), 'affiliation': author.findtext('organization'), } diff --git a/ietf/wsgi.py b/ietf/wsgi.py index c43334874b..bd17da5ba0 100644 --- a/ietf/wsgi.py +++ b/ietf/wsgi.py @@ -1,23 +1,17 @@ -# Copyright The IETF Trust 2013-2021, All Rights Reserved +# Copyright The IETF Trust 2013-2024, All Rights Reserved # -*- coding: utf-8 -*- - import os import sys -import syslog path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -syslog.openlog(str("datatracker"), syslog.LOG_PID, syslog.LOG_USER) - if not path in sys.path: sys.path.insert(0, path) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ietf.settings") -syslog.syslog("Starting datatracker wsgi instance") - from django.core.wsgi import get_wsgi_application -application = get_wsgi_application() +application = get_wsgi_application() diff --git a/jsconfig.json b/jsconfig.json index 0898bb2e2a..da44ff2fb2 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -15,7 +15,7 @@ "vueCompilerOptions": { "target": 3, "plugins": [ - "@volar/vue-language-plugin-pug" + "@vue/language-plugin-pug" ] } } diff --git a/k8s/auth.yaml b/k8s/auth.yaml new file mode 100644 index 0000000000..8aa1d53cbd --- /dev/null +++ b/k8s/auth.yaml @@ -0,0 +1,116 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: auth +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: auth + strategy: + type: Recreate + template: + metadata: + labels: + app: auth + spec: + securityContext: + runAsNonRoot: true + containers: + # ----------------------------------------------------- + # ScoutAPM Container + # ----------------------------------------------------- + - name: scoutapm + image: "scoutapp/scoutapm:version-1.4.0" + imagePullPolicy: IfNotPresent + # Replace command with one that will shut down on a TERM signal + # The ./core-agent start command line is from the scoutapm docker image + command: + - "sh" + - "-c" + - >- + trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM; + ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 & + wait $! + livenessProbe: + exec: + command: + - "sh" + - "-c" + - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'" + securityContext: + readOnlyRootFilesystem: true + runAsUser: 65534 # "nobody" user by default + runAsGroup: 65534 # "nogroup" group by default + # ----------------------------------------------------- + # Datatracker Container + # ----------------------------------------------------- + - name: datatracker + image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" + imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http + protocol: TCP + volumeMounts: + - name: dt-vol + mountPath: /a + - name: dt-tmp + mountPath: /tmp + - name: dt-home + mountPath: /home/datatracker + - name: dt-xml2rfc-cache + mountPath: /var/cache/xml2rfc + - name: dt-cfg + mountPath: /workspace/ietf/settings_local.py + subPath: settings_local.py + env: + - name: "CONTAINER_ROLE" + value: "datatracker" + # ensures the pod gets recreated on every deploy: + - name: "DEPLOY_UID" + value: "$DEPLOY_UID" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumes: + # To be overriden with the actual shared volume + - name: dt-vol + - name: dt-tmp + emptyDir: + sizeLimit: "2Gi" + - name: dt-xml2rfc-cache + emptyDir: + sizeLimit: "2Gi" + - name: dt-home + emptyDir: + sizeLimit: "2Gi" + - name: dt-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 60 +--- +apiVersion: v1 +kind: Service +metadata: + name: auth +spec: + type: ClusterIP + ports: + - port: 8080 + targetPort: http + protocol: TCP + name: http + selector: + app: auth diff --git a/k8s/beat.yaml b/k8s/beat.yaml new file mode 100644 index 0000000000..99317ab77a --- /dev/null +++ b/k8s/beat.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: beat + labels: + deleteBeforeUpgrade: yes +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: beat + strategy: + type: Recreate + template: + metadata: + labels: + app: beat + spec: + securityContext: + runAsNonRoot: true + containers: + - name: beat + image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" + imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http + protocol: TCP + volumeMounts: + - name: dt-vol + mountPath: /a + - name: dt-tmp + mountPath: /tmp + - name: dt-cfg + mountPath: /workspace/ietf/settings_local.py + subPath: settings_local.py + env: + - name: "CONTAINER_ROLE" + value: "beat" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumes: + # To be overriden with the actual shared volume + - name: dt-vol + - name: dt-tmp + emptyDir: + sizeLimit: "2Gi" + - name: dt-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 600 diff --git a/k8s/celery.yaml b/k8s/celery.yaml new file mode 100644 index 0000000000..dfb20fa40a --- /dev/null +++ b/k8s/celery.yaml @@ -0,0 +1,101 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: celery + labels: + deleteBeforeUpgrade: yes +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: celery + strategy: + type: Recreate + template: + metadata: + labels: + app: celery + spec: + securityContext: + runAsNonRoot: true + containers: + # ----------------------------------------------------- + # ScoutAPM Container + # ----------------------------------------------------- + - name: scoutapm + image: "scoutapp/scoutapm:version-1.4.0" + imagePullPolicy: IfNotPresent + # Replace command with one that will shut down on a TERM signal + # The ./core-agent start command line is from the scoutapm docker image + command: + - "sh" + - "-c" + - >- + trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM; + ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 & + wait $! + livenessProbe: + exec: + command: + - "sh" + - "-c" + - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'" + securityContext: + readOnlyRootFilesystem: true + runAsUser: 65534 # "nobody" user by default + runAsGroup: 65534 # "nogroup" group by default + # ----------------------------------------------------- + # Celery Container + # ----------------------------------------------------- + - name: celery + image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" + imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http + protocol: TCP + volumeMounts: + - name: dt-vol + mountPath: /a + - name: dt-tmp + mountPath: /tmp + - name: dt-home + mountPath: /home/datatracker + - name: dt-xml2rfc-cache + mountPath: /var/cache/xml2rfc + - name: dt-cfg + mountPath: /workspace/ietf/settings_local.py + subPath: settings_local.py + env: + - name: "CONTAINER_ROLE" + value: "celery" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumes: + # To be overriden with the actual shared volume + - name: dt-vol + - name: dt-tmp + emptyDir: + sizeLimit: "2Gi" + - name: dt-xml2rfc-cache + emptyDir: + sizeLimit: "2Gi" + - name: dt-home + emptyDir: + sizeLimit: "2Gi" + - name: dt-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 600 diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml new file mode 100644 index 0000000000..5ad4336614 --- /dev/null +++ b/k8s/datatracker.yaml @@ -0,0 +1,145 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: datatracker +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: datatracker + strategy: + type: Recreate + template: + metadata: + labels: + app: datatracker + spec: + securityContext: + runAsNonRoot: true + containers: + # ----------------------------------------------------- + # ScoutAPM Container + # ----------------------------------------------------- + - name: scoutapm + image: "scoutapp/scoutapm:version-1.4.0" + imagePullPolicy: IfNotPresent + # Replace command with one that will shut down on a TERM signal + # The ./core-agent start command line is from the scoutapm docker image + command: + - "sh" + - "-c" + - >- + trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM; + ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 & + wait $! + livenessProbe: + exec: + command: + - "sh" + - "-c" + - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'" + securityContext: + readOnlyRootFilesystem: true + runAsUser: 65534 # "nobody" user by default + runAsGroup: 65534 # "nogroup" group by default + # ----------------------------------------------------- + # Datatracker Container + # ----------------------------------------------------- + - name: datatracker + image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" + imagePullPolicy: Always + ports: + - containerPort: 8000 + name: http + protocol: TCP + volumeMounts: + - name: dt-vol + mountPath: /a + - name: dt-tmp + mountPath: /tmp + - name: dt-home + mountPath: /home/datatracker + - name: dt-xml2rfc-cache + mountPath: /var/cache/xml2rfc + - name: dt-cfg + mountPath: /workspace/ietf/settings_local.py + subPath: settings_local.py + env: + - name: "CONTAINER_ROLE" + value: "datatracker" + # ensures the pod gets recreated on every deploy: + - name: "DEPLOY_UID" + value: "$DEPLOY_UID" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + initContainers: + - name: migration + image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" + env: + - name: "CONTAINER_ROLE" + value: "migrations" + envFrom: + - configMapRef: + name: django-config + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: dt-vol + mountPath: /a + - name: dt-tmp + mountPath: /tmp + - name: dt-home + mountPath: /home/datatracker + - name: dt-xml2rfc-cache + mountPath: /var/cache/xml2rfc + - name: dt-cfg + mountPath: /workspace/ietf/settings_local.py + subPath: settings_local.py + volumes: + # To be overriden with the actual shared volume + - name: dt-vol + - name: dt-tmp + emptyDir: + sizeLimit: "2Gi" + - name: dt-xml2rfc-cache + emptyDir: + sizeLimit: "2Gi" + - name: dt-home + emptyDir: + sizeLimit: "2Gi" + - name: dt-cfg + configMap: + name: files-cfgmap + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 60 +--- +apiVersion: v1 +kind: Service +metadata: + name: datatracker +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app: datatracker diff --git a/k8s/django-config.yaml b/k8s/django-config.yaml new file mode 100644 index 0000000000..07e2d710de --- /dev/null +++ b/k8s/django-config.yaml @@ -0,0 +1,83 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: django-config +data: + # n.b., these are debug values / non-secret secrets + DATATRACKER_SERVER_MODE: "development" # development for staging, production for production + DATATRACKER_ADMINS: |- + Robert Sparks + Ryan Cross + Kesara Rathnayake + Jennifer Richards + Nicolas Giard + DATATRACKER_ALLOWED_HOSTS: ".ietf.org" # newline-separated list also allowed + # DATATRACKER_DATATRACKER_DEBUG: "false" + + # DB access details - needs to be filled in + # DATATRACKER_DB_HOST: "db" + # DATATRACKER_DB_PORT: "5432" + # DATATRACKER_DB_NAME: "datatracker" + # DATATRACKER_DB_USER: "django" # secret + # DATATRACKER_DB_PASS: "RkTkDPFnKpko" # secret + # DATATRACKER_DB_CONN_MAX_AGE: "0" # connection per request if not set, no limit if set to "None" + # DATATRACKER_DB_CONN_HEALTH_CHECKS: "false" + + DATATRACKER_DJANGO_SECRET_KEY: "PDwXboUq!=hPjnrtG2=ge#N$Dwy+wn@uivrugwpic8mxyPfHk" # secret + + # Set this to point testing / staging at the production statics server until we + # sort that out + # DATATRACKER_STATIC_URL: "https://static.ietf.org/dt/12.10.0/" + + # DATATRACKER_EMAIL_DEBUG: "true" + + # Outgoing email details + # DATATRACKER_EMAIL_HOST: "localhost" # defaults to localhost + # DATATRACKER_EMAIL_PORT: "2025" # defaults to 2025 + + # The value here is the default from settings.py (i.e., not actually secret) + DATATRACKER_NOMCOM_APP_SECRET_B64: "m9pzMezVoFNJfsvU9XSZxGnXnwup6P5ZgCQeEnROOoQ=" # secret + + DATATRACKER_IANA_SYNC_PASSWORD: "this-is-the-iana-sync-password" # secret + DATATRACKER_RFC_EDITOR_SYNC_PASSWORD: "this-is-the-rfc-editor-sync-password" # secret + DATATRACKER_YOUTUBE_API_KEY: "this-is-the-youtube-api-key" # secret + DATATRACKER_GITHUB_BACKUP_API_KEY: "this-is-the-github-backup-api-key" # secret + + # API key configuration + DATATRACKER_API_KEY_TYPE: "ES265" + # secret - value here is the default from settings.py (i.e., not actually secret) + DATATRACKER_API_PUBLIC_KEY_PEM_B64: |- + Ci0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tCk1Ga3dFd1lIS29aSXpqMENBUVlJS + 29aSXpqMERBUWNEUWdBRXFWb2pzYW9mREpTY3VNSk4rdHNodW15Tk01TUUKZ2Fyel + ZQcWtWb3ZtRjZ5RTdJSi9kdjRGY1YrUUtDdEovck9TOGUzNlk4WkFFVll1dWtoZXM + weVoxdz09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo= + # secret - value here is the default from settings.py (i.e., not actually secret) + DATATRACKER_API_PRIVATE_KEY_PEM_B64: |- + Ci0tLS0tQkVHSU4gUFJJVkFURSBLRVktLS0tLQpNSUdIQWdFQU1CTUdCeXFHU000O + UFnRUdDQ3FHU000OUF3RUhCRzB3YXdJQkFRUWdvSTZMSmtvcEtxOFhySGk5ClFxR1 + F2RTRBODNURllqcUx6KzhnVUxZZWNzcWhSQU5DQUFTcFdpT3hxaDhNbEp5NHdrMzY + yeUc2Ykkwemt3U0IKcXZOVStxUldpK1lYcklUc2duOTIvZ1Z4WDVBb0swbitzNUx4 + N2ZwanhrQVJWaTY2U0Y2elRKblgKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo= + + #DATATRACKER_REGISTRATION_API_KEY: "some-key" # secret" + + # DATATRACKER_MEETECHO_API_BASE: "https://meetings.conf.meetecho.com/api/v1/" + DATATRACKER_MEETECHO_CLIENT_ID: "this-is-the-meetecho-client-id" # secret + DATATRACKER_MEETECHO_CLIENT_SECRET: "this-is-the-meetecho-client-secret" # secret + + # DATATRACKER_MATOMO_SITE_ID: "7" # must be present to enable Matomo + # DATATRACKER_MATOMO_DOMAIN_PATH: "analytics.ietf.org" + + CELERY_PASSWORD: "this-is-a-secret" # secret + + # Only one of these may be set + # DATATRACKER_APP_API_TOKENS_JSON_B64: "e30K" # secret + # DATATRACKER_APP_API_TOKENS_JSON: "{}" # secret + + # use this to override default - one entry per line + # DATATRACKER_CSRF_TRUSTED_ORIGINS: |- + # https://datatracker.staging.ietf.org + + # Scout configuration + DATATRACKER_SCOUT_KEY: "this-is-the-scout-key" + DATATRACKER_SCOUT_NAME: "StagingDatatracker" diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml new file mode 100644 index 0000000000..cfc17f35d7 --- /dev/null +++ b/k8s/kustomization.yaml @@ -0,0 +1,14 @@ +namespace: datatracker +namePrefix: dt- +configMapGenerator: + - name: files-cfgmap + files: + - settings_local.py +resources: + - auth.yaml + - beat.yaml + - celery.yaml + - datatracker.yaml + - django-config.yaml + - memcached.yaml + - rabbitmq.yaml diff --git a/k8s/memcached.yaml b/k8s/memcached.yaml new file mode 100644 index 0000000000..e94066c9e0 --- /dev/null +++ b/k8s/memcached.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: memcached +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: memcached + template: + metadata: + labels: + app: memcached + spec: + securityContext: + runAsNonRoot: true + containers: + - image: "quay.io/prometheus/memcached-exporter:v0.14.3" + imagePullPolicy: IfNotPresent + name: memcached-exporter + ports: + - name: metrics + containerPort: 9150 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 65534 # nobody + runAsGroup: 65534 # nobody + - image: "memcached:1.6-alpine" + imagePullPolicy: IfNotPresent + args: ["-m", "1024"] + name: memcached + ports: + - name: memcached + containerPort: 11211 + protocol: TCP + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + # memcached image sets up uid/gid 11211 + runAsUser: 11211 + runAsGroup: 11211 + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 +--- +apiVersion: v1 +kind: Service +metadata: + name: memcached + annotations: + k8s.grafana.com/scrape: "true" # this is not a bool + k8s.grafana.com/metrics.portName: "metrics" +spec: + type: ClusterIP + ports: + - port: 11211 + targetPort: memcached + protocol: TCP + name: memcached + - port: 9150 + targetPort: metrics + protocol: TCP + name: metrics + selector: + app: memcached diff --git a/k8s/rabbitmq.yaml b/k8s/rabbitmq.yaml new file mode 100644 index 0000000000..132ca79ded --- /dev/null +++ b/k8s/rabbitmq.yaml @@ -0,0 +1,175 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: rabbitmq +spec: + replicas: 1 + revisionHistoryLimit: 2 + selector: + matchLabels: + app: rabbitmq + template: + metadata: + labels: + app: rabbitmq + spec: + securityContext: + runAsNonRoot: true + initContainers: + # ----------------------------------------------------- + # Init RabbitMQ data + # ----------------------------------------------------- + - name: init-rabbitmq + image: busybox:stable + command: + - "sh" + - "-c" + - "mkdir -p -m700 /mnt/rabbitmq && chown 100:101 /mnt/rabbitmq" + securityContext: + runAsNonRoot: false + runAsUser: 0 + readOnlyRootFilesystem: true + volumeMounts: + - name: "rabbitmq-data" + mountPath: "/mnt" + containers: + # ----------------------------------------------------- + # RabbitMQ Container + # ----------------------------------------------------- + - image: "ghcr.io/ietf-tools/datatracker-mq:3.12-alpine" + imagePullPolicy: Always + name: rabbitmq + ports: + - name: amqp + containerPort: 5672 + protocol: TCP + volumeMounts: + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq + subPath: "rabbitmq" + - name: rabbitmq-tmp + mountPath: /tmp + - name: rabbitmq-config + mountPath: "/etc/rabbitmq" + env: + - name: "CELERY_PASSWORD" + value: "this-is-a-secret" + livenessProbe: + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + periodSeconds: 30 + timeoutSeconds: 5 + startupProbe: + initialDelaySeconds: 15 + periodSeconds: 5 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 60 + exec: + command: ["rabbitmq-diagnostics", "-q", "ping"] + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + # rabbitmq image sets up uid/gid 100/101 + runAsUser: 100 + runAsGroup: 101 + volumes: + - name: rabbitmq-tmp + emptyDir: + sizeLimit: "50Mi" + - name: rabbitmq-config + configMap: + name: "rabbitmq-configmap" + dnsPolicy: ClusterFirst + restartPolicy: Always + terminationGracePeriodSeconds: 30 + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + # storageClassName: "" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: rabbitmq-configmap +data: + definitions.json: |- + { + "permissions": [ + { + "configure": ".*", + "read": ".*", + "user": "datatracker", + "vhost": "dt", + "write": ".*" + } + ], + "users": [ + { + "hashing_algorithm": "rabbit_password_hashing_sha256", + "limits": {}, + "name": "datatracker", + "password_hash": "HJxcItcpXtBN+R/CH7dUelfKBOvdUs3AWo82SBw2yLMSguzb", + "tags": [] + } + ], + "vhosts": [ + { + "limits": [], + "metadata": { + "description": "", + "tags": [] + }, + "name": "dt" + } + ] + } + rabbitmq.conf: |- + # prevent guest from logging in over tcp + loopback_users.guest = true + + # load saved definitions + load_definitions = /etc/rabbitmq/definitions.json + + # Ensure that enough disk is available to flush to disk. To do this, need to limit the + # memory available to the container to something reasonable. See + # https://www.rabbitmq.com/production-checklist.html#monitoring-and-resource-usage + # for recommendations. + + # 1-1.5 times the memory available to the container is adequate for disk limit + disk_free_limit.absolute = 6000MB + + # This should be ~40% of the memory available to the container. Use an + # absolute number because relative will be proprtional to the full machine + # memory. + vm_memory_high_watermark.absolute = 1600MB + + # Logging + log.file = false + log.console = true + log.console.level = info + log.console.formatter = json +--- +apiVersion: v1 +kind: Service +metadata: + name: rabbitmq +spec: + type: ClusterIP + clusterIP: None # headless service + ports: + - port: 5672 + targetPort: amqp + protocol: TCP + name: amqp + selector: + app: rabbitmq diff --git a/k8s/settings_local.py b/k8s/settings_local.py new file mode 100644 index 0000000000..6f0956d065 --- /dev/null +++ b/k8s/settings_local.py @@ -0,0 +1,275 @@ +# Copyright The IETF Trust 2007-2024, All Rights Reserved +# -*- coding: utf-8 -*- + +from base64 import b64decode +from email.utils import parseaddr +import json + +from ietf import __release_hash__ +from ietf.settings import * # pyflakes:ignore + + +def _multiline_to_list(s): + """Helper to split at newlines and conver to list""" + return [item.strip() for item in s.split("\n")] + + +# Default to "development". Production _must_ set DATATRACKER_SERVER_MODE="production" in the env! +SERVER_MODE = os.environ.get("DATATRACKER_SERVER_MODE", "development") + +# Secrets +_SECRET_KEY = os.environ.get("DATATRACKER_DJANGO_SECRET_KEY", None) +if _SECRET_KEY is not None: + SECRET_KEY = _SECRET_KEY +else: + raise RuntimeError("DATATRACKER_DJANGO_SECRET_KEY must be set") + +_NOMCOM_APP_SECRET_B64 = os.environ.get("DATATRACKER_NOMCOM_APP_SECRET_B64", None) +if _NOMCOM_APP_SECRET_B64 is not None: + NOMCOM_APP_SECRET = b64decode(_NOMCOM_APP_SECRET_B64) +else: + raise RuntimeError("DATATRACKER_NOMCOM_APP_SECRET_B64 must be set") + +_IANA_SYNC_PASSWORD = os.environ.get("DATATRACKER_IANA_SYNC_PASSWORD", None) +if _IANA_SYNC_PASSWORD is not None: + IANA_SYNC_PASSWORD = _IANA_SYNC_PASSWORD +else: + raise RuntimeError("DATATRACKER_IANA_SYNC_PASSWORD must be set") + +_RFC_EDITOR_SYNC_PASSWORD = os.environ.get("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD", None) +if _RFC_EDITOR_SYNC_PASSWORD is not None: + RFC_EDITOR_SYNC_PASSWORD = os.environ.get("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD") +else: + raise RuntimeError("DATATRACKER_RFC_EDITOR_SYNC_PASSWORD must be set") + +_YOUTUBE_API_KEY = os.environ.get("DATATRACKER_YOUTUBE_API_KEY", None) +if _YOUTUBE_API_KEY is not None: + YOUTUBE_API_KEY = _YOUTUBE_API_KEY +else: + raise RuntimeError("DATATRACKER_YOUTUBE_API_KEY must be set") + +_GITHUB_BACKUP_API_KEY = os.environ.get("DATATRACKER_GITHUB_BACKUP_API_KEY", None) +if _GITHUB_BACKUP_API_KEY is not None: + GITHUB_BACKUP_API_KEY = _GITHUB_BACKUP_API_KEY +else: + raise RuntimeError("DATATRACKER_GITHUB_BACKUP_API_KEY must be set") + +_API_KEY_TYPE = os.environ.get("DATATRACKER_API_KEY_TYPE", None) +if _API_KEY_TYPE is not None: + API_KEY_TYPE = _API_KEY_TYPE +else: + raise RuntimeError("DATATRACKER_API_KEY_TYPE must be set") + +_API_PUBLIC_KEY_PEM_B64 = os.environ.get("DATATRACKER_API_PUBLIC_KEY_PEM_B64", None) +if _API_PUBLIC_KEY_PEM_B64 is not None: + API_PUBLIC_KEY_PEM = b64decode(_API_PUBLIC_KEY_PEM_B64) +else: + raise RuntimeError("DATATRACKER_API_PUBLIC_KEY_PEM_B64 must be set") + +_API_PRIVATE_KEY_PEM_B64 = os.environ.get("DATATRACKER_API_PRIVATE_KEY_PEM_B64", None) +if _API_PRIVATE_KEY_PEM_B64 is not None: + API_PRIVATE_KEY_PEM = b64decode(_API_PRIVATE_KEY_PEM_B64) +else: + raise RuntimeError("DATATRACKER_API_PRIVATE_KEY_PEM_B64 must be set") + +# Set DEBUG if DATATRACKER_DEBUG env var is the word "true" +DEBUG = os.environ.get("DATATRACKER_DEBUG", "false").lower() == "true" + +# DATATRACKER_ALLOWED_HOSTS env var is a comma-separated list of allowed hosts +_allowed_hosts_str = os.environ.get("DATATRACKER_ALLOWED_HOSTS", None) +if _allowed_hosts_str is not None: + ALLOWED_HOSTS = _multiline_to_list(_allowed_hosts_str) + +DATABASES = { + "default": { + "HOST": os.environ.get("DATATRACKER_DB_HOST", "db"), + "PORT": os.environ.get("DATATRACKER_DB_PORT", "5432"), + "NAME": os.environ.get("DATATRACKER_DB_NAME", "datatracker"), + "ENGINE": "django.db.backends.postgresql", + "USER": os.environ.get("DATATRACKER_DB_USER", "django"), + "PASSWORD": os.environ.get("DATATRACKER_DB_PASS", ""), + "OPTIONS": json.loads(os.environ.get("DATATRACKER_DB_OPTS_JSON", "{}")), + }, +} + +# Configure persistent connections. A setting of 0 is Django's default. +_conn_max_age = os.environ.get("DATATRACKER_DB_CONN_MAX_AGE", "0") +# A string "none" means unlimited age. +DATABASES["default"]["CONN_MAX_AGE"] = None if _conn_max_age.lower() == "none" else int(_conn_max_age) +# Enable connection health checks if DATATRACKER_DB_CONN_HEALTH_CHECK is the string "true" +_conn_health_checks = bool( + os.environ.get("DATATRACKER_DB_CONN_HEALTH_CHECKS", "false").lower() == "true" +) +DATABASES["default"]["CONN_HEALTH_CHECKS"] = _conn_health_checks + +# DATATRACKER_ADMINS is a newline-delimited list of addresses parseable by email.utils.parseaddr +_admins_str = os.environ.get("DATATRACKER_ADMINS", None) +if _admins_str is not None: + ADMINS = [parseaddr(admin) for admin in _multiline_to_list(_admins_str)] +else: + raise RuntimeError("DATATRACKER_ADMINS must be set") + +USING_DEBUG_EMAIL_SERVER = os.environ.get("DATATRACKER_EMAIL_DEBUG", "false").lower() == "true" +EMAIL_HOST = os.environ.get("DATATRACKER_EMAIL_HOST", "localhost") +EMAIL_PORT = int(os.environ.get("DATATRACKER_EMAIL_PORT", "2025")) + +_celery_password = os.environ.get("CELERY_PASSWORD", None) +if _celery_password is None: + raise RuntimeError("CELERY_PASSWORD must be set") +CELERY_BROKER_URL = "amqp://datatracker:{password}@{host}/{queue}".format( + host=os.environ.get("RABBITMQ_HOSTNAME", "dt-rabbitmq"), + password=_celery_password, + queue=os.environ.get("RABBITMQ_QUEUE", "dt") +) + +IANA_SYNC_USERNAME = "ietfsync" +IANA_SYNC_CHANGES_URL = "https://datatracker.iana.org:4443/data-tracker/changes" +IANA_SYNC_PROTOCOLS_URL = "http://www.iana.org/protocols/" + +RFC_EDITOR_NOTIFICATION_URL = "http://www.rfc-editor.org/parser/parser.php" + +_registration_api_key = os.environ.get("DATATRACKER_REGISTRATION_API_KEY", None) +if _registration_api_key is None: + raise RuntimeError("DATATRACKER_REGISTRATION_API_KEY must be set") +STATS_REGISTRATION_ATTENDEES_JSON_URL = f"https://registration.ietf.org/{{number}}/attendees/?apikey={_registration_api_key}" + +#FIRST_CUTOFF_DAYS = 12 +#SECOND_CUTOFF_DAYS = 12 +#SUBMISSION_CUTOFF_DAYS = 26 +#SUBMISSION_CORRECTION_DAYS = 57 +MEETING_MATERIALS_SUBMISSION_CUTOFF_DAYS = 26 +MEETING_MATERIALS_SUBMISSION_CORRECTION_DAYS = 54 + +# disable htpasswd by setting to a do-nothing command +HTPASSWD_COMMAND = "/bin/true" + +_MEETECHO_CLIENT_ID = os.environ.get("DATATRACKER_MEETECHO_CLIENT_ID", None) +_MEETECHO_CLIENT_SECRET = os.environ.get("DATATRACKER_MEETECHO_CLIENT_SECRET", None) +if _MEETECHO_CLIENT_ID is not None and _MEETECHO_CLIENT_SECRET is not None: + MEETECHO_API_CONFIG = { + "api_base": os.environ.get( + "DATATRACKER_MEETECHO_API_BASE", + "https://meetings.conf.meetecho.com/api/v1/", + ), + "client_id": _MEETECHO_CLIENT_ID, + "client_secret": _MEETECHO_CLIENT_SECRET, + "request_timeout": 3.01, # python-requests doc recommend slightly > a multiple of 3 seconds + } +else: + raise RuntimeError( + "DATATRACKER_MEETECHO_CLIENT_ID and DATATRACKER_MEETECHO_CLIENT_SECRET must be set" + ) + +# For APP_API_TOKENS, ccept either base64-encoded JSON or raw JSON, but not both +if "DATATRACKER_APP_API_TOKENS_JSON_B64" in os.environ: + if "DATATRACKER_APP_API_TOKENS_JSON" in os.environ: + raise RuntimeError( + "Only one of DATATRACKER_APP_API_TOKENS_JSON and DATATRACKER_APP_API_TOKENS_JSON_B64 may be set" + ) + _APP_API_TOKENS_JSON = b64decode(os.environ.get("DATATRACKER_APP_API_TOKENS_JSON_B64")) +else: + _APP_API_TOKENS_JSON = os.environ.get("DATATRACKER_APP_API_TOKENS_JSON", None) + +if _APP_API_TOKENS_JSON is not None: + APP_API_TOKENS = json.loads(_APP_API_TOKENS_JSON) +else: + APP_API_TOKENS = {} + +EMAIL_COPY_TO = "" + +# Until we teach the datatracker to look beyond cloudflare for this check +IDSUBMIT_MAX_DAILY_SAME_SUBMITTER = 5000 + +# Leave DATATRACKER_MATOMO_SITE_ID unset to disable Matomo reporting +if "DATATRACKER_MATOMO_SITE_ID" in os.environ: + MATOMO_DOMAIN_PATH = os.environ.get("DATATRACKER_MATOMO_DOMAIN_PATH", "analytics.ietf.org") + MATOMO_SITE_ID = os.environ.get("DATATRACKER_MATOMO_SITE_ID") + MATOMO_DISABLE_COOKIES = True + +# Leave DATATRACKER_SCOUT_KEY unset to disable Scout APM agent +_SCOUT_KEY = os.environ.get("DATATRACKER_SCOUT_KEY", None) +if _SCOUT_KEY is not None: + if SERVER_MODE == "production": + PROD_PRE_APPS = ["scout_apm.django", ] + else: + DEV_PRE_APPS = ["scout_apm.django", ] + SCOUT_MONITOR = True + SCOUT_KEY = _SCOUT_KEY + SCOUT_NAME = os.environ.get("DATATRACKER_SCOUT_NAME", "Datatracker") + SCOUT_ERRORS_ENABLED = True + SCOUT_SHUTDOWN_MESSAGE_ENABLED = False + SCOUT_CORE_AGENT_SOCKET_PATH = "tcp://{host}:{port}".format( + host=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_HOST", "localhost"), + port=os.environ.get("DATATRACKER_SCOUT_CORE_AGENT_PORT", "6590"), + ) + SCOUT_CORE_AGENT_DOWNLOAD = False + SCOUT_CORE_AGENT_LAUNCH = False + SCOUT_REVISION_SHA = __release_hash__[:7] + +STATIC_URL = os.environ.get("DATATRACKER_STATIC_URL", None) +if STATIC_URL is None: + from ietf import __version__ + STATIC_URL = f"https://static.ietf.org/dt/{__version__}/" + +# Set these to the same as "production" in settings.py, whether production mode or not +MEDIA_ROOT = "/a/www/www6s/lib/dt/media/" +MEDIA_URL = "https://www.ietf.org/lib/dt/media/" +PHOTOS_DIRNAME = "photo" +PHOTOS_DIR = MEDIA_ROOT + PHOTOS_DIRNAME + +# Normally only set for debug, but needed until we have a real FS +DJANGO_VITE_MANIFEST_PATH = os.path.join(BASE_DIR, 'static/dist-neue/manifest.json') + +# Binaries that are different in the docker image +DE_GFM_BINARY = "/usr/local/bin/de-gfm" +IDSUBMIT_IDNITS_BINARY = "/usr/local/bin/idnits" + +# Duplicating production cache from settings.py and using it whether we're in production mode or not +MEMCACHED_HOST = os.environ.get("DT_MEMCACHED_SERVICE_HOST", "127.0.0.1") +MEMCACHED_PORT = os.environ.get("DT_MEMCACHED_SERVICE_PORT", "11211") +from ietf import __version__ +CACHES = { + "default": { + "BACKEND": "ietf.utils.cache.LenientMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", + "VERSION": __version__, + "KEY_PREFIX": "ietf:dt", + "KEY_FUNCTION": lambda key, key_prefix, version: ( + f"{key_prefix}:{version}:{sha384(str(key).encode('utf8')).hexdigest()}" + ), + }, + "sessions": { + "BACKEND": "ietf.utils.cache.LenientMemcacheCache", + "LOCATION": f"{MEMCACHED_HOST}:{MEMCACHED_PORT}", + # No release-specific VERSION setting. + "KEY_PREFIX": "ietf:dt", + }, + "htmlized": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/htmlized", + "OPTIONS": { + "MAX_ENTRIES": 100000, # 100,000 + }, + }, + "pdfized": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/pdfized", + "OPTIONS": { + "MAX_ENTRIES": 100000, # 100,000 + }, + }, + "slowpages": { + "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": "/a/cache/datatracker/slowpages", + "OPTIONS": { + "MAX_ENTRIES": 5000, + }, + }, +} + +_csrf_trusted_origins_str = os.environ.get("DATATRACKER_CSRF_TRUSTED_ORIGINS") +if _csrf_trusted_origins_str is not None: + CSRF_TRUSTED_ORIGINS = _multiline_to_list(_csrf_trusted_origins_str) + +# Console logs as JSON instead of plain when running in k8s +LOGGING["handlers"]["console"]["formatter"] = "json" diff --git a/package.json b/package.json index ec174c680d..afcabd13ed 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "bootstrap": "5.3.3", "bootstrap-icons": "1.11.3", "browser-fs-access": "0.35.0", - "caniuse-lite": "1.0.30001597", - "d3": "7.8.5", + "caniuse-lite": "1.0.30001603", + "d3": "7.9.0", "file-saver": "2.0.5", "highcharts": "11.4.0", "ical.js": "1.5.0", @@ -56,6 +56,7 @@ "@parcel/transformer-sass": "2.12.0", "@rollup/pluginutils": "5.1.0", "@vitejs/plugin-vue": "4.6.2", + "@vue/language-plugin-pug": "2.0.7", "browserlist": "latest", "c8": "9.1.0", "eslint": "8.57.0", @@ -65,14 +66,14 @@ "eslint-plugin-n": "16.6.2", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.1.1", - "eslint-plugin-vue": "9.22.0", - "html-validate": "8.15.0", + "eslint-plugin-vue": "9.24.0", + "html-validate": "8.18.1", "jquery-migrate": "3.4.1", "parcel": "2.12.0", "pug": "3.0.2", - "sass": "1.71.1", + "sass": "1.72.0", "seedrandom": "3.0.5", - "vite": "4.5.2" + "vite": "4.5.3" }, "targets": { "ietf": { diff --git a/patch/add-django-cprofile-filter.patch b/patch/add-django-cprofile-filter.patch index 128d5a9f09..bf684a0b33 100644 --- a/patch/add-django-cprofile-filter.patch +++ b/patch/add-django-cprofile-filter.patch @@ -1,15 +1,9 @@ ---- django_cprofile_middleware/middleware.py.old 2018-04-04 06:32:29.282187502 -0700 -+++ django_cprofile_middleware/middleware.py 2018-04-06 10:11:18.936855634 -0700 -@@ -1,4 +1,5 @@ - import pstats -+import re - - try: - import cProfile as profile -@@ -14,6 +15,15 @@ - from django.utils.deprecation import MiddlewareMixin - - +--- django_cprofile_middleware/middleware.py.old 2024-06-27 21:03:56.975128007 +0000 ++++ django_cprofile_middleware/middleware.py 2024-06-27 23:45:59.421683008 +0000 +@@ -19,6 +19,16 @@ + from django_cprofile_middleware.utils import MiddlewareMixin + + +class Stats(pstats.Stats): + def filter_stats(self, regex): + oldstats = self.stats @@ -18,17 +12,64 @@ + for func, (cc, nc, tt, ct, callers) in oldstats.iteritems(): + if filter.search(pstats.func_std_string(func)): + newstats[func] = (cc, nc, tt, ct, callers) ++ + class ProfilerMiddleware(MiddlewareMixin): """ Simple profile middleware to profile django views. To run it, add ?prof to -@@ -62,8 +72,13 @@ +@@ -38,9 +48,11 @@ + ?download => Download profile file suitable for visualization. For example + in snakeviz or RunSnakeRun + +- This is adapted from an example found here: +- http://www.slideshare.net/zeeg/django-con-high-performance-django-presentation. ++ Patched with https://github.com/omarish/django-cprofile-middleware/pull/23 ++ for operation with Django 4.2.5+ + """ ++ PROFILER_REQUEST_ATTR_NAME = '_django_cprofile_middleware_profiler' ++ + def can(self, request): + requires_staff = getattr( + settings, "DJANGO_CPROFILE_MIDDLEWARE_REQUIRE_STAFF", True) +@@ -52,10 +64,11 @@ + + def process_view(self, request, callback, callback_args, callback_kwargs): + if self.can(request): +- self.profiler = profile.Profile() ++ profiler = profile.Profile() ++ setattr(request, self.PROFILER_REQUEST_ATTR_NAME, profiler) + args = (request,) + callback_args + try: +- return self.profiler.runcall( ++ return profiler.runcall( + callback, *args, **callback_kwargs) + except Exception: + # we want the process_exception middleware to fire +@@ -63,12 +76,13 @@ + return + + def process_response(self, request, response): +- if self.can(request): +- self.profiler.create_stats() ++ if hasattr(request, self.PROFILER_REQUEST_ATTR_NAME): ++ profiler = getattr(request, self.PROFILER_REQUEST_ATTR_NAME) ++ profiler.create_stats() + if 'download' in request.GET: + import marshal + +- output = marshal.dumps(self.profiler.stats) ++ output = marshal.dumps(profiler.stats) + response = HttpResponse( + output, content_type='application/octet-stream') + response['Content-Disposition'] = 'attachment;' \ +@@ -76,9 +90,14 @@ response['Content-Length'] = len(output) else: io = StringIO() - stats = pstats.Stats(self.profiler, stream=io) ++ stats = Stats(profiler, stream=io) + - stats.strip_dirs().sort_stats(request.GET.get('sort', 'time')) -+ stats = Stats(self.profiler, stream=io) + if request.GET.get('stripdirs', False): + stats = stats.strip_dirs() + filter = request.GET.get('filter', None) @@ -36,5 +77,5 @@ + stats.filter_stats(filter) + stats.sort_stats(request.GET.get('psort') or 'time') stats.print_stats(int(request.GET.get('count', 100))) + response = HttpResponse('
    %s
    ' % io.getvalue()) - return response diff --git a/playwright/package-lock.json b/playwright/package-lock.json index e150f79e28..abe2518ef2 100644 --- a/playwright/package-lock.json +++ b/playwright/package-lock.json @@ -22,7 +22,7 @@ "eslint-plugin-n": "16.6.2", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.1.1", - "npm-check-updates": "16.14.15" + "npm-check-updates": "16.14.18" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -563,6 +563,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/semver-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/semver-utils/-/semver-utils-1.1.3.tgz", + "integrity": "sha512-T+YwkslhsM+CeuhYUxyAjWm7mJ5am/K10UX40RuA6k6Lc7eGtq8iY2xOzy7Vq0GOqhl/xZl5l2FwURZMTPTUww==", + "dev": true + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -3804,11 +3810,12 @@ } }, "node_modules/npm-check-updates": { - "version": "16.14.15", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", - "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", + "version": "16.14.18", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.18.tgz", + "integrity": "sha512-9iaRe9ohx9ykdbLjPRIYcq1A0RkrPYUx9HmQK1JIXhfxtJCNE/+497H9Z4PGH6GWRALbz5KF+1iZoySK2uSEpQ==", "dev": true, "dependencies": { + "@types/semver-utils": "^1.1.1", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "commander": "^10.0.1", @@ -6347,6 +6354,12 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "@types/semver-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/semver-utils/-/semver-utils-1.1.3.tgz", + "integrity": "sha512-T+YwkslhsM+CeuhYUxyAjWm7mJ5am/K10UX40RuA6k6Lc7eGtq8iY2xOzy7Vq0GOqhl/xZl5l2FwURZMTPTUww==", + "dev": true + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -8721,11 +8734,12 @@ } }, "npm-check-updates": { - "version": "16.14.15", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.15.tgz", - "integrity": "sha512-WH0wJ9j6CP7Azl+LLCxWAYqroT2IX02kRIzgK/fg0rPpMbETgHITWBdOPtrv521xmA3JMgeNsQ62zvVtS/nCmQ==", + "version": "16.14.18", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.18.tgz", + "integrity": "sha512-9iaRe9ohx9ykdbLjPRIYcq1A0RkrPYUx9HmQK1JIXhfxtJCNE/+497H9Z4PGH6GWRALbz5KF+1iZoySK2uSEpQ==", "dev": true, "requires": { + "@types/semver-utils": "^1.1.1", "chalk": "^5.3.0", "cli-table3": "^0.6.3", "commander": "^10.0.1", diff --git a/playwright/package.json b/playwright/package.json index a660215275..874faa824e 100644 --- a/playwright/package.json +++ b/playwright/package.json @@ -14,7 +14,7 @@ "eslint-plugin-n": "16.6.2", "eslint-plugin-node": "11.1.0", "eslint-plugin-promise": "6.1.1", - "npm-check-updates": "16.14.15" + "npm-check-updates": "16.14.18" }, "dependencies": { "@faker-js/faker": "8.4.1", diff --git a/requirements.txt b/requirements.txt index c27b3adce4..8187c1cebf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,8 +8,6 @@ bleach>=6 types-bleach>=6 celery>=5.2.6 coverage>=4.5.4,<5.0 # Coverage 5.x moves from a json database to SQLite. Moving to 5.x will require substantial rewrites in ietf.utils.test_runner and ietf.release.views -decorator>=5.1.1 -types-decorator>=5.1.1 defusedxml>=0.7.1 # for TastyPie when using xml; not a declared dependency Django>4.2,<5 django-analytical>=3.1.0 @@ -22,7 +20,7 @@ django-markup>=1.5 # Limited use - need to reconcile against direct use of ma django-oidc-provider>=0.8.1 # 0.8 dropped Django 2 support django-referrer-policy>=1.0 django-simple-history>=3.0.0 -django-stubs>=4.2.7 # The django-stubs version used determines the the mypy version indicated below +django-stubs>=4.2.7,<5 # The django-stubs version used determines the the mypy version indicated below django-tastypie>=0.14.5 # Version must be locked in sync with version of Django django-vite>=2.0.2,<3 django-widget-tweaks>=1.4.12 @@ -55,10 +53,12 @@ pyopenssl>=22.0.0 # Used by urllib3.contrib, which is used by PyQuery but not pyquery>=1.4.3 python-dateutil>=2.8.2 types-python-dateutil>=2.8.2 +python-json-logger>=2.0.7 python-magic==0.4.18 # Versions beyond the yanked .19 and .20 introduce form failures -pymemcache>=4.0.0 # for django.core.cache.backends.memcached.PyMemcacheCache +pymemcache>=4.0.0 # for django.core.cache.backends.memcached.PyMemcacheCache python-mimeparse>=1.6 # from TastyPie pytz==2022.2.1 # Pinned as changes need to be vetted for their effect on Meeting fields +types-pytz==2022.2.1 # match pytz version requests>=2.31.0 types-requests>=2.27.1 requests-mock>=1.9.3 diff --git a/yarn.lock b/yarn.lock index 0282b82d9e..54768ac391 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2019,6 +2019,43 @@ __metadata: languageName: node linkType: hard +"@volar/language-core@npm:2.1.4": + version: 2.1.4 + resolution: "@volar/language-core@npm:2.1.4" + dependencies: + "@volar/source-map": 2.1.4 + checksum: 7430f651431ed00eb7489d48c0596f4653fe70da3c779acfaa5807051db4491c9e4e154e9f0de3c9d863a3b4b1194a517a75395ca9134ea2b1b8af5ff637b204 + languageName: node + linkType: hard + +"@volar/language-service@npm:~2.1.0": + version: 2.1.4 + resolution: "@volar/language-service@npm:2.1.4" + dependencies: + "@volar/language-core": 2.1.4 + vscode-languageserver-protocol: ^3.17.5 + vscode-languageserver-textdocument: ^1.0.11 + vscode-uri: ^3.0.8 + checksum: 06cdcfacf0fab22cee652cab1ae1729628d7ebf68f5f9e791e19e3715b2a4775c0bd2ec2e7a9b0815d93f244d7a745f3ea41aa5084923b10e9258a5f54c1107b + languageName: node + linkType: hard + +"@volar/source-map@npm:2.1.4, @volar/source-map@npm:~2.1.3": + version: 2.1.4 + resolution: "@volar/source-map@npm:2.1.4" + dependencies: + muggle-string: ^0.4.0 + checksum: e2f65bcfd667a02ee5cfe49e612b12e75c05fdaecf3b3590fdd7a0255dce7e51d09e8d4c390c2098ca7321cea219c16a8ea3f6c0f36ca9c0edff3975990b458b + languageName: node + linkType: hard + +"@vscode/l10n@npm:^0.0.18": + version: 0.0.18 + resolution: "@vscode/l10n@npm:0.0.18" + checksum: c33876cebdef0385359619200ecb5d7c46d7f9abffb80f9fab1f83abb5d6bfdb44cc6d792d1b1b9c736c729121274733bbdcd5d2d2eea0d157bdf662d521edef + languageName: node + linkType: hard + "@vue/compiler-core@npm:3.4.21": version: 3.4.21 resolution: "@vue/compiler-core@npm:3.4.21" @@ -2083,6 +2120,16 @@ __metadata: languageName: node linkType: hard +"@vue/language-plugin-pug@npm:2.0.7": + version: 2.0.7 + resolution: "@vue/language-plugin-pug@npm:2.0.7" + dependencies: + "@volar/source-map": ~2.1.3 + volar-service-pug: 0.0.34 + checksum: 11cc96eb5f240144e91b27fe06fcd48de4ef1e4c7fe666d1173b346ed64b7edfa922bd4eb2e512a91a0c6b907975afcaf69cfee4c91af11168590142b3aba4c3 + languageName: node + linkType: hard + "@vue/reactivity@npm:3.4.21": version: 3.4.21 resolution: "@vue/reactivity@npm:3.4.21" @@ -2637,10 +2684,10 @@ browserlist@latest: languageName: node linkType: hard -"caniuse-lite@npm:1.0.30001597": - version: 1.0.30001597 - resolution: "caniuse-lite@npm:1.0.30001597" - checksum: ec6a2cf0fd49f37d16732e6595939fc80a125dcd188a950bc936c61b4ad53becc0fe51bf2d9a625415de7b1cb23bd835f220e8b68d8ab951a940edeea65476fd +"caniuse-lite@npm:1.0.30001603": + version: 1.0.30001603 + resolution: "caniuse-lite@npm:1.0.30001603" + checksum: e66e0d24b899c2ed3fdcc2dd44df29c4fc06d74fa8f43abe81fc7cff4a72b092d438e0fb5b7daeb252ee267519f32c6c7d229a15e7a4f4263afef6ea3832b661 languageName: node linkType: hard @@ -3216,9 +3263,9 @@ browserlist@latest: languageName: node linkType: hard -"d3@npm:7.8.5": - version: 7.8.5 - resolution: "d3@npm:7.8.5" +"d3@npm:7.9.0": + version: 7.9.0 + resolution: "d3@npm:7.9.0" dependencies: d3-array: 3 d3-axis: 3 @@ -3250,7 +3297,7 @@ browserlist@latest: d3-timer: 3 d3-transition: 3 d3-zoom: 3 - checksum: e407e79731f74d946a5eb8dec2f037b5a4ad33c294409b1d3531fdf7094de48adfe364974cb37e2396bdb81e23149d56d0ede716c004d6aebb52b3cc114cd15c + checksum: 1c0e9135f1fb78aa32b187fafc8b56ae6346102bd0e4e5e5a5339611a51e6038adbaa293fae373994228100eddd87320e930b1be922baeadc07c9fd43d26d99b languageName: node linkType: hard @@ -3938,11 +3985,12 @@ browserlist@latest: languageName: node linkType: hard -"eslint-plugin-vue@npm:9.22.0": - version: 9.22.0 - resolution: "eslint-plugin-vue@npm:9.22.0" +"eslint-plugin-vue@npm:9.24.0": + version: 9.24.0 + resolution: "eslint-plugin-vue@npm:9.24.0" dependencies: "@eslint-community/eslint-utils": ^4.4.0 + globals: ^13.24.0 natural-compare: ^1.4.0 nth-check: ^2.1.1 postcss-selector-parser: ^6.0.15 @@ -3951,7 +3999,7 @@ browserlist@latest: xml-name-validator: ^4.0.0 peerDependencies: eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 - checksum: 5f1e94b412567b8d0bb99c89ea1032d7801d1aa34344e61d82b71c926d3ced049861b484c31c641606923246d482972a45e690236e5479f46b9736d432545ecd + checksum: 2309b919d8fced6210c11e09107f443990063c0392843909cf50fad682e820c48bf5cc28b82a1239c03fd7ceeb4239e1baa653370c4c76689ec5fb8a970cd303 languageName: node linkType: hard @@ -4667,9 +4715,9 @@ browserlist@latest: languageName: node linkType: hard -"html-validate@npm:8.15.0": - version: 8.15.0 - resolution: "html-validate@npm:8.15.0" +"html-validate@npm:8.18.1": + version: 8.18.1 + resolution: "html-validate@npm:8.18.1" dependencies: "@babel/code-frame": ^7.10.0 "@html-validate/stylish": ^4.1.0 @@ -4698,7 +4746,7 @@ browserlist@latest: optional: true bin: html-validate: bin/html-validate.js - checksum: 0af7685ca1302cbcbbaebae771b64048267aa3ce26fe279f99647b4febd03373e1fbb85cc3e5ba2b0986741eecb6a2b6f5a3143615b6d8407e28210199ec136a + checksum: 53479bf75bcb6ad748a6543583de6a26bfb55d85c0ae793bd6619c0079795f482c01b4168a7dea2584219f31b8a05c3ea2a0d5ebfd639099caf623263d3ac536 languageName: node linkType: hard @@ -5968,6 +6016,13 @@ browserlist@latest: languageName: node linkType: hard +"muggle-string@npm:^0.4.0": + version: 0.4.1 + resolution: "muggle-string@npm:0.4.1" + checksum: 85fe1766d18d43cf22b6da7d047203a65b2e2b1ccfac505b699c2a459644f95ebb3c854a96db5be559eea0e213f6ee32b986b8c2f73c48e6c89e1fd829616532 + languageName: node + linkType: hard + "murmurhash-js@npm:1.0.0": version: 1.0.0 resolution: "murmurhash-js@npm:1.0.0" @@ -6982,13 +7037,14 @@ browserlist@latest: "@rollup/pluginutils": 5.1.0 "@twuni/emojify": 1.0.2 "@vitejs/plugin-vue": 4.6.2 + "@vue/language-plugin-pug": 2.0.7 bootstrap: 5.3.3 bootstrap-icons: 1.11.3 browser-fs-access: 0.35.0 browserlist: latest c8: 9.1.0 - caniuse-lite: 1.0.30001597 - d3: 7.8.5 + caniuse-lite: 1.0.30001603 + d3: 7.9.0 eslint: 8.57.0 eslint-config-standard: 17.1.0 eslint-plugin-cypress: 2.15.1 @@ -6996,10 +7052,10 @@ browserlist@latest: eslint-plugin-n: 16.6.2 eslint-plugin-node: 11.1.0 eslint-plugin-promise: 6.1.1 - eslint-plugin-vue: 9.22.0 + eslint-plugin-vue: 9.24.0 file-saver: 2.0.5 highcharts: 11.4.0 - html-validate: 8.15.0 + html-validate: 8.18.1 ical.js: 1.5.0 jquery: 3.7.1 jquery-migrate: 3.4.1 @@ -7017,7 +7073,7 @@ browserlist@latest: pinia: 2.1.7 pinia-plugin-persist: 1.0.0 pug: 3.0.2 - sass: 1.71.1 + sass: 1.72.0 seedrandom: 3.0.5 select2: 4.1.0-rc.0 select2-bootstrap-5-theme: 1.3.0 @@ -7026,7 +7082,7 @@ browserlist@latest: slugify: 1.6.6 sortablejs: 1.15.2 vanillajs-datepicker: 1.3.4 - vite: 4.5.2 + vite: 4.5.3 vue: 3.4.21 vue-router: 4.3.0 zxcvbn: 4.4.2 @@ -7093,16 +7149,16 @@ browserlist@latest: languageName: node linkType: hard -"sass@npm:1.71.1": - version: 1.71.1 - resolution: "sass@npm:1.71.1" +"sass@npm:1.72.0": + version: 1.72.0 + resolution: "sass@npm:1.72.0" dependencies: chokidar: ">=3.0.0 <4.0.0" immutable: ^4.0.0 source-map-js: ">=0.6.2 <2.0.0" bin: sass: sass.js - checksum: 19c4939d3042eb9459d462bbd27b1f576fa18034e23c87ca0005b87effdee431c16503b5a785edcdcde1a76dfb804716d9ad42c85a78968ac3825d515e45cb53 + checksum: f420079c7d51660b7256ee52463c1499ede36f7fd5c8ef50c687451777ad641509001454dea45244073cedd7c00e7a3bc1c362e55206ac6686171b994edb41e4 languageName: node linkType: hard @@ -7821,9 +7877,9 @@ browserlist@latest: languageName: node linkType: hard -"vite@npm:4.5.2": - version: 4.5.2 - resolution: "vite@npm:4.5.2" +"vite@npm:4.5.3": + version: 4.5.3 + resolution: "vite@npm:4.5.3" dependencies: esbuild: ^0.18.10 fsevents: ~2.3.2 @@ -7857,7 +7913,7 @@ browserlist@latest: optional: true bin: vite: bin/vite.js - checksum: 9d1f84f703c2660aced34deee7f309278ed368880f66e9570ac115c793d91f7fffb80ab19c602b3c8bc1341fe23437d86a3fcca2a9ef82f7ef0cdac5a40d0c86 + checksum: fd3f512ce48ca2a1fe60ad0376283b832de9272725fdbc65064ae9248f792de87b0f27a89573115e23e26784800daca329f8a9234d298ba6f60e808a9c63883c languageName: node linkType: hard @@ -7868,6 +7924,36 @@ browserlist@latest: languageName: node linkType: hard +"volar-service-html@npm:0.0.34": + version: 0.0.34 + resolution: "volar-service-html@npm:0.0.34" + dependencies: + vscode-html-languageservice: ^5.1.0 + vscode-languageserver-textdocument: ^1.0.11 + vscode-uri: ^3.0.8 + peerDependencies: + "@volar/language-service": ~2.1.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 83b50cd805680c77b5632e9534b23cddb85bf7e0cd425624d474981d173ddf07a66fcce6348f675c9d5c2551df9ae1e58206c2ed1c32052f8a70940fb7f5fe50 + languageName: node + linkType: hard + +"volar-service-pug@npm:0.0.34": + version: 0.0.34 + resolution: "volar-service-pug@npm:0.0.34" + dependencies: + "@volar/language-service": ~2.1.0 + pug-lexer: ^5.0.1 + pug-parser: ^6.0.0 + volar-service-html: 0.0.34 + vscode-html-languageservice: ^5.1.0 + vscode-languageserver-textdocument: ^1.0.11 + checksum: 4691aa1c8ea9039e1b5ce4218445309575c2cb4bc08ad5341a8af6f0db1a60711f26cc905e124c3485cc780eb58b895332fbb6a2ccf427a9d0e08012f2c5ad4a + languageName: node + linkType: hard + "vooks@npm:^0.2.12, vooks@npm:^0.2.4": version: 0.2.12 resolution: "vooks@npm:0.2.12" @@ -7879,6 +7965,56 @@ browserlist@latest: languageName: node linkType: hard +"vscode-html-languageservice@npm:^5.1.0": + version: 5.1.2 + resolution: "vscode-html-languageservice@npm:5.1.2" + dependencies: + "@vscode/l10n": ^0.0.18 + vscode-languageserver-textdocument: ^1.0.11 + vscode-languageserver-types: ^3.17.5 + vscode-uri: ^3.0.8 + checksum: 3a2a5ee5ad4ea429e85f4fb8f45da5b47d50541784d703fc9ccd009f68426034a48be6c04f8c420dc7236de07df93ccc28873da3395db5f5626fe169f18f1ac6 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: f302a01e59272adc1ae6494581fa31c15499f9278df76366e3b97b2236c7c53ebfc71efbace9041cfd2caa7f91675b9e56f2407871a1b3c7f760a2e2ee61484a + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:^3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + checksum: dfb42d276df5dfea728267885b99872ecff62f6c20448b8539fae71bb196b420f5351c5aca7c1047bf8fb1f89fa94a961dce2bc5bf7e726198f4be0bb86a1e71 + languageName: node + linkType: hard + +"vscode-languageserver-textdocument@npm:^1.0.11": + version: 1.0.11 + resolution: "vscode-languageserver-textdocument@npm:1.0.11" + checksum: ea7cdc9d4ffaae5952071fa11d17d714215a76444e6936c9359f94b9ba3222a52a55edb5bd5928bd3e9712b900a9f175bb3565ec1c8923234fe3bd327584bafb + languageName: node + linkType: hard + +"vscode-languageserver-types@npm:3.17.5, vscode-languageserver-types@npm:^3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-types@npm:3.17.5" + checksum: 79b420e7576398d396579ca3a461c9ed70e78db4403cd28bbdf4d3ed2b66a2b4114031172e51fad49f0baa60a2180132d7cb2ea35aa3157d7af3c325528210ac + languageName: node + linkType: hard + +"vscode-uri@npm:^3.0.8": + version: 3.0.8 + resolution: "vscode-uri@npm:3.0.8" + checksum: 514249126850c0a41a7d8c3c2836cab35983b9dc1938b903cfa253b9e33974c1416d62a00111385adcfa2b98df456437ab704f709a2ecca76a90134ef5eb4832 + languageName: node + linkType: hard + "vue-demi@npm:>=0.14.5": version: 0.14.5 resolution: "vue-demi@npm:0.14.5"