diff --git a/.env.example b/.env.example index 0d68c711..4ce921ac 100644 --- a/.env.example +++ b/.env.example @@ -1,17 +1,20 @@ NODE_ENV=development -APPLICATIONINSIGHTS_CONNECTION_STRING= +APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://localhost/ -BASIC_GITHUB_TOKEN= +# Optional, but without it GitHub rate limiting will usually stop the rulesets +# and codelists downloading at startup. +GITHUB_OAUTH_APP_CLIENT_ID= +GITHUB_OAUTH_APP_CLIENT_SECRET= REDIS_PORT=6379 REDIS_HOSTNAME=redis # replace with localhost if you run outside docker -VALIDATOR_SERVICES_URL=https://dev-func-validator-services.azurewebsites.net/api +VALIDATOR_SERVICES_URL=https://func-validator-services-dev.azurewebsites.net/api VALIDATOR_SERVICES_KEY_NAME=x-functions-key VALIDATOR_SERVICES_KEY_VALUE= -DATASTORE_SERVICES_URL=https://dev-func-datastore-services.azurewebsites.net/api +DATASTORE_SERVICES_URL=https://func-datastore-services-dev.azurewebsites.net/api DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME=x-functions-key DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE= DATASTORE_SERVICES_IATI_IDENTIFIERS_EXIST_MAX_NUMBER_OF_IDS=5000 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e110e605..950b60ad 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,6 +6,15 @@ updates: directory: '/' schedule: interval: 'daily' + ignore: + # Pinned exactly, and upgraded by hand. A libxml2-wasm release can carry a different + # bundled libxml2, which is what decides whether a file is schema valid - and since + # v0.7.0 its libxml2 submodule tracks the maintainer's fork rather than an upstream + # release tag, so a bump also takes whatever else is on that branch. PR CI does not + # run the differential harness that verified verdicts against the Dashboard, and + # dependabot PRs auto-merge once CI is green, so a bump would otherwise land with no + # human reading it. See README "XML Library". + - dependency-name: 'libxml2-wasm' # Enable version updates for GitHub Actions - package-ecosystem: 'github-actions' diff --git a/.github/workflows/develop-func-ci.yml b/.github/workflows/develop-func-ci.yml index 47e966a4..3f8cc0bb 100644 --- a/.github/workflows/develop-func-ci.yml +++ b/.github/workflows/develop-func-ci.yml @@ -12,6 +12,30 @@ on: - main jobs: + # Running the integration tests against the Docker container requires temporarily + # setting "authLevel": "anonymous" in the function.json files (see README), because + # the container has no AzureWebJobsStorage and so cannot validate function keys. + # Committing that would leave the deployed endpoints reachable without a key. + # There is a matching .husky/pre-commit guard, but hooks are skippable with + # --no-verify and are absent in a fresh clone until `npm install` has run. + check-auth-level: + runs-on: ubuntu-latest + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v7 + + - name: 'Reject authLevel anonymous in function.json' + run: | + if grep -il '"authLevel"[[:space:]]*:[[:space:]]*"anonymous"' */function.json > /tmp/hits; then + echo "::error::function.json committed with authLevel anonymous - the deployed endpoint would need no function key" + echo "Offending files:" + cat /tmp/hits + echo "" + echo "Restore with: git checkout -- '*/function.json'" + exit 1 + fi + echo "OK - all function.json files require a function key." + build-test-node: runs-on: ubuntu-latest steps: @@ -21,10 +45,10 @@ jobs: sudo apt-get install libxml2-utils - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Setup Node Environment' - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' cache: 'npm' @@ -34,26 +58,33 @@ jobs: npm i -g npm@^10 npm ci + # Runs before the unit tests because it catches a class they cannot: a source file + # importing a package that is not installed. No unit test imports utils.js - it + # pulls in Redis - so a tree where utils.js and package.json disagree about the XML + # library still reports every test passing. + - name: 'Lint' + run: npx eslint . + - name: 'Run unit tests' - run: npm run rules:test + run: npm run unit:test build-docker: runs-on: ubuntu-latest steps: - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Build Docker Image' run: | docker build . -t validator-local:latest automerge: - needs: [build-test-node, build-docker] + needs: [check-auth-level, build-test-node, build-docker] runs-on: ubuntu-latest permissions: pull-requests: write contents: write steps: - - uses: fastify/github-action-merge-dependabot@v3.9.1 + - uses: fastify/github-action-merge-dependabot@v3.15.0 with: github-token: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/develop-func-deploy.yml b/.github/workflows/develop-func-deploy.yml index 48d1fe00..d2bd6c05 100644 --- a/.github/workflows/develop-func-deploy.yml +++ b/.github/workflows/develop-func-deploy.yml @@ -32,7 +32,8 @@ env: ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} KV_URI: ${{ secrets.DEV_KV_URI }} - BASIC_GITHUB_TOKEN: ${{ secrets.BASIC_GITHUB_TOKEN }} + GITHUB_OAUTH_APP_CLIENT_ID: ${{ secrets.IATI_GITHUB_OAUTH_APP_CLIENT_ID }} + GITHUB_OAUTH_APP_CLIENT_SECRET: ${{ secrets.IATI_GITHUB_OAUTH_APP_CLIENT_SECRET }} DATASTORE_SERVICES_URL: ${{ secrets.DEV_DATASTORE_SERVICES_URL }} DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME: ${{ secrets.DEV_DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME }} DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE: ${{ secrets.DEV_DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE }} @@ -48,15 +49,15 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Login via Azure CLI' - uses: azure/login@v1.4.7 + uses: azure/login@v3.0.1 with: creds: ${{ env.AZURE_CREDENTIALS }} # Service Principal credentials saved in GitHub Secrets - name: 'Docker Login' - uses: azure/docker-login@v1 + uses: azure/docker-login@v2 with: login-server: ${{ env.ACR_LOGIN_SERVER }} username: ${{ env.ACR_USERNAME }} @@ -134,8 +135,13 @@ jobs: "slotSetting": false }, { - "name": "BASIC_GITHUB_TOKEN", - "value": "${{ env.BASIC_GITHUB_TOKEN }}", + "name": "GITHUB_OAUTH_APP_CLIENT_ID", + "value": "${{ env.GITHUB_OAUTH_APP_CLIENT_ID }}", + "slotSetting": false + }, + { + "name": "GITHUB_OAUTH_APP_CLIENT_SECRET", + "value": "${{ env.GITHUB_OAUTH_APP_CLIENT_SECRET }}", "slotSetting": false }, { @@ -172,7 +178,7 @@ jobs: image: ${{ env.ACR_LOGIN_SERVER }}/func-${{ env.NAME}}-${{ env.STAGE }}:${{ env.TAG }} - name: 'Tag resource for Tech Climate Impact' - uses: 'azure/CLI@v1.0.8' + uses: 'azure/CLI@v3.0.0' with: azcliversion: 2.30.0 inlineScript: | @@ -190,7 +196,7 @@ jobs: run: sleep 180 - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Install newman' run: | diff --git a/.github/workflows/prod-func-deploy.yml b/.github/workflows/prod-func-deploy.yml index 59b109db..a1d02ffb 100644 --- a/.github/workflows/prod-func-deploy.yml +++ b/.github/workflows/prod-func-deploy.yml @@ -22,7 +22,8 @@ env: ACR_USERNAME: ${{ secrets.ACR_USERNAME }} ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }} KV_URI: ${{ secrets.PROD_KV_URI }} - BASIC_GITHUB_TOKEN: ${{ secrets.BASIC_GITHUB_TOKEN }} + GITHUB_OAUTH_APP_CLIENT_ID: ${{ secrets.IATI_GITHUB_OAUTH_APP_CLIENT_ID }} + GITHUB_OAUTH_APP_CLIENT_SECRET: ${{ secrets.IATI_GITHUB_OAUTH_APP_CLIENT_SECRET }} DATASTORE_SERVICES_URL: ${{ secrets.PROD_DATASTORE_SERVICES_URL }} DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME: ${{ secrets.PROD_DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME }} DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE: ${{ secrets.PROD_DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE }} @@ -33,15 +34,15 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Login via Azure CLI' - uses: azure/login@v1.4.7 + uses: azure/login@v3.0.1 with: creds: ${{ env.AZURE_CREDENTIALS }} # Service Principal credentials saved in GitHub Secrets - name: 'Docker Login' - uses: azure/docker-login@v1 + uses: azure/docker-login@v2 with: login-server: ${{ env.ACR_LOGIN_SERVER }} username: ${{ env.ACR_USERNAME }} @@ -119,8 +120,13 @@ jobs: "slotSetting": false }, { - "name": "BASIC_GITHUB_TOKEN", - "value": "${{ env.BASIC_GITHUB_TOKEN }}", + "name": "GITHUB_OAUTH_APP_CLIENT_ID", + "value": "${{ env.GITHUB_OAUTH_APP_CLIENT_ID }}", + "slotSetting": false + }, + { + "name": "GITHUB_OAUTH_APP_CLIENT_SECRET", + "value": "${{ env.GITHUB_OAUTH_APP_CLIENT_SECRET }}", "slotSetting": false }, { @@ -157,7 +163,7 @@ jobs: image: ${{ env.ACR_LOGIN_SERVER }}/func-${{ env.NAME}}-${{ env.STAGE }}:${{ env.TAG }} - name: 'Tag resource for Tech Climate Impact' - uses: 'azure/CLI@v1.0.8' + uses: 'azure/CLI@v3.0.0' with: azcliversion: 2.30.0 inlineScript: | @@ -175,7 +181,7 @@ jobs: run: sleep 300 - name: 'Checkout GitHub Action' - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: 'Install newman' run: | diff --git a/.husky/pre-commit b/.husky/pre-commit index d37daa07..dbb75f86 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,29 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" +# Running the integration tests against the Docker container requires temporarily +# setting "authLevel": "anonymous" in the function.json files (see README). The +# container has no AzureWebJobsStorage, so it cannot validate function keys and +# every request 401s otherwise. +# +# Committing that change would leave the deployed endpoints reachable without a +# function key. This guard refuses the commit instead of relying on memory. +# +# Restore with: git checkout -- '*/function.json' +for f in $(git diff --cached --name-only --diff-filter=ACM | grep 'function\.json$'); do + if git show ":$f" | grep -qi '"authLevel"[[:space:]]*:[[:space:]]*"anonymous"'; then + echo "" + echo "ERROR $f is staged with \"authLevel\": \"anonymous\"" + echo "" + echo " That would leave the deployed endpoint unauthenticated." + echo " It is only meant to be set temporarily, to run the" + echo " integration tests against the Docker container." + echo "" + echo " Restore all of them with:" + echo " git checkout -- '*/function.json'" + echo "" + exit 1 + fi +done + npx --no-install lint-staged diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..2c89ff91 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,3 @@ +# AGENTS.md + +Dev deployments are often deferred rather than immediate; if a push to `develop` did not deploy, see the "Deploying to dev" section of README.md before investigating. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5194240b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,53 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). + +## [Unreleased] + +### Added + +### Changed + +### Fixed + +### Removed + +## [2.5.0] - 2026-08-24 + +### Changed + +- Replaced libxmljs2 with libxml2-wasm, moving from libxml2 2.9.9 (2019) to + 2.15.1. libxmljs2 is no longer maintained and no release of it carries a newer + libxml2, so the library was replaced rather than upgraded. +- **`?details=true` responses have changed shape.** Error details are now + `{ line, column }`, where they were the raw libxml2 error struct. See Removed. +- Schemas are now compiled once at startup rather than re-parsed for every + activity, so validation of files with schema errors should be faster. + +### Added + +- Schema unit tests (`schema-unit-tests`) and tests for file parsing and + metadata (`utils-unit-tests`). Neither area had any coverage before. +- A pre-commit hook and CI check that refuse a commit setting + `"authLevel": "anonymous"` in any `function.json`. That value is only meant to + be set temporarily, to run the integration tests against the Docker container. + +### Fixed + +- Schema validation: sign-only decimals such as ` - ` are now + reported; over-long decimals no longer are. This is the divergence with the + IATI Dashboard reported in + [#574](https://github.com/IATI/js-validator-api/issues/574). +- `.env.example`: corrected the dev service hostnames + +### Removed + +- `libxmljs2`, and with it the native build dependency — no node-gyp, + prebuilt binaries or glibc coupling. +- `str1`, `code`, `level`, `domain` and `int1` from the `details` object + returned by `?details=true`. `str1` still appears within the error message; + `level` and `domain` were constant; `str2`, `str3` and `int1` were never + populated. `code`, the numeric libxml2 constraint identifier, has no + replacement — libxml2-wasm does not expose it. diff --git a/README.md b/README.md index 4f98b22f..0841698d 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,17 @@ If it has warnings or errors, you'll see them listed. APPLICATIONINSIGHTS_CONNECTION_STRING -- Needs to be set for running locally, but will not actually report telemetry to the AppInsights instance in my experience +- **Required — the app will not start without it.** `config/appInsights.js` calls + `appInsights.setup(...).start()` at module load. If the value is empty or malformed the SDK throws + `Instrumentation key not found`, the Functions worker fails to load every function that imports it, + and requests get `503 Function host is not running` (or an empty 500) naming no cause. + For local development any syntactically valid connection string works; no telemetry is sent. The + value shipped in `.env.example` is a usable dummy: -BASIC_GITHUB_TOKEN +GITHUB_OAUTH_APP_CLIENT_ID +GITHUB_OAUTH_APP_CLIENT_SECRET -- GitHub personal access token. This is needed to pull in the Codelists from the `IATI/IATI-Validator-Codelists` repository. Note that you cannot use a "Personal Access Token (Classic)"; you must generate a fine-grained access token. +- GitHub OAuth app. No special permissions or access needed. Optional, but you may get rate limited very easily if you don't. REDIS_PORT=6379 REDIS_HOSTNAME=redis @@ -108,7 +114,7 @@ VALIDATOR_SERVICES_KEY_VALUE= - URL and API Key for Validator Services, used to get list of Publisher Identifiers -DATASTORE_SERVICES_URL=https://dev-func-datastore-services.azurewebsites.net/api +DATASTORE_SERVICES_URL=https://func-datastore-services-dev.azurewebsites.net/api DATASTORE_SERVICES_AUTH_HTTP_HEADER_NAME=x-functions-key DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE= DATASTORE_SERVICES_IATI_IDENTIFIERS_EXIST_MAX_NUMBER_OF_IDS=5000 @@ -116,6 +122,11 @@ DATASTORE_SERVICES_IATI_IDENTIFIERS_EXIST_MAX_NUMBER_OF_IDS=5000 - URL and API Key for datastore services, used by the advisory system to check for the existence of IATI Identifiers in the Datastore +- VALIDATOR_SERVICES_KEY_VALUE and DATASTORE_SERVICES_AUTH_HTTP_HEADER_VALUE are outbound + credentials for Azure Functions. If you are an authorised developer on this + codebase you can get them from the Azure portal for the respective Function Apps. + If you do not have Azure access, ask the team. + ### App config defaults (set in `config/config.js`) ``` @@ -165,7 +176,7 @@ let myEnvVariable = config.ENV_VAR ## Unit Tests -- `npm run rules:test` +- `npm run unit:test` There is a large set of Mocha unit tests for the Rulesets logic in `ruleset-unit-tests`. @@ -184,7 +195,7 @@ In Docker container - Install newman globally `npm i -g newman` - Edit `function.json` files to set `"authLevel": "anonymous"`, don't forget to change back! - Start function `npm run docker:start` -- Run Tests `npm docker:int:test` +- Run Tests `npm run docker:int:test` ### Modifying/Adding @@ -202,6 +213,39 @@ Using files: https://github.com/IATI/IATI-Internal-Wiki#development-process +### Deploying to dev + +Dev deploys are handled by `.github/workflows/develop-func-deploy.yml`, which triggers on push to `develop` and on a daily 04:19 UTC cron. + +- Dependabot PRs are auto-merged with `GITHUB_TOKEN`. GitHub never triggers workflows on `GITHUB_TOKEN` pushes, so those merges are deployed by the next daily cron, not immediately. +- This is a public repo, so GitHub auto-disables any workflow with a `schedule` trigger after 60 days without repo activity. When that happens the whole deploy workflow **stops responding to all** triggers, including human pushes to `develop`. Pushing again does not re-enable it. +- The workflow will be re-enabled when a commit touches the workflow file itself (e.g. a dependabot action bump), after which the next 04:19 UTC cron deploys. It can also be re-enabled via the GitHub UI, or via this command: + + `gh workflow enable develop-func-deploy.yml` + +- If a merge to `develop` does not deploy, check the workflow state: + + `gh api repos/IATI/js-validator-api/actions/workflows --jq '.workflows[] | "\(.state) \(.path)"'` + + If the deploy workflow is not `active`, use one of the methods detailed above to re-enable the workflow, then do a manual dispatch. + +## XML Library + +XML parsing and XSD schema validation use [`libxml2-wasm`](https://github.com/jameslan/libxml2-wasm), a WebAssembly build of libxml2. `libxml2-wasm` builds its libxml2 from a git submodule, and from v0.7.0 that submodule points at the maintainer's own fork rather than upstream — earlier releases such as v0.6.0 pinned a clean upstream release tag. Version 0.7.1 pins commit `f52e859`, which is the **v2.15.1 release plus two unmerged commits** by the `libxml2-wasm` maintainer adding Windows path handling. It contains all of 2.15.1, but sits on a branch off it, so those two commits are not in 2.15.2 or 2.15.3. + +Those patches touch `uri.c` and `xmlIO.c`, which is the code resolving `xsd:include`, but they do not change behaviour here: every branch they add is guarded by a runtime flag that defaults to off, and `libxml2-wasm` explicitly disables it on any platform other than Windows (`node_modules/libxml2-wasm/lib/libxml2.mjs`). Behaviour was also compared against v0.6.0, which pins the clean upstream v2.14.5 tag, across 426 real published datasets with no disagreement in verdict or error count. + +When upgrading `libxml2-wasm`, check whether the submodule has returned to an upstream tag. While it stays on the fork, each bump also takes whatever else is on that branch. + +It replaced the native `libxmljs2`, which is no longer maintained and still bundles libxml2 2.9.9 from 2019 — old enough to accept values that later versions correctly reject, so the Validator disagreed with the Dashboard on whether a file was schema valid. No release of `libxmljs2` carries a newer libxml2, which is why the library was changed rather than upgraded. + +Two things to know when working with it: + +- Documents and compiled schemas hold memory outside the JS heap and must be `dispose()`d. Whatever creates an `XmlDocument` is responsible for freeing it, normally in a `finally`. +- It throws on a parse failure whenever libxml2 recorded any error at all, even when a usable document was still produced. `libxmljs2` threw only when no document could be built, so problems that do not prevent a tree being built — an undeclared namespace prefix, say — were reported later as schema errors. `utils/xmlParse.js` restores that behaviour; without it such files would be rejected with a file level `0.1.1` instead. + +Note the `xmllint --recover` pre-pass is a separate, system-installed libxml2 (see Prerequisities), so it is generally an older version than the one used for validation. + ## Customised Dependencies ### xpath diff --git a/config/config.js b/config/config.js index 9e7439f6..33fb4c4e 100644 --- a/config/config.js +++ b/config/config.js @@ -11,7 +11,8 @@ const config = { NS_PER_SEC: 1e9, VERSIONS: process.env.VERSIONS || ['2.01', '2.02', '2.03'], MAX_FILESIZE: process.env.MAX_FILESIZE || 60, - BASIC_GITHUB_TOKEN: process.env.BASIC_GITHUB_TOKEN, + GITHUB_OAUTH_APP_CLIENT_ID: process.env.GITHUB_OAUTH_APP_CLIENT_ID, + GITHUB_OAUTH_APP_CLIENT_SECRET: process.env.GITHUB_OAUTH_APP_CLIENT_SECRET, REDIS_PORT: process.env.REDIS_PORT || 6379, REDIS_CACHE_SEC: process.env.REDIS_CACHE_SEC || 86400, REDIS_KEY: process.env.REDIS_KEY, diff --git a/integration-tests/js-validator-api-tests.postman_collection.json b/integration-tests/js-validator-api-tests.postman_collection.json index aff6257c..d1caaeac 100644 --- a/integration-tests/js-validator-api-tests.postman_collection.json +++ b/integration-tests/js-validator-api-tests.postman_collection.json @@ -405,6 +405,10 @@ "pm.test(\"Raw feedback from the xml2js parser should be displayed in the error message\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.map(obj => (obj.message))).to.include(\"Start tag expected, '<' not found\\n\");", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 1\");", "});" ], "type": "text/javascript" @@ -473,9 +477,19 @@ " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.map(obj => (obj.message))).to.include(\"Start tag expected, '<' not found\\n\");", "});", - "pm.test(\"Error object contains a details object\", function () {", + "pm.test(\"Details object contains the line and column of the parse failure\", function () {", " var jsonData = pm.response.json();", - " pm.expect(jsonData.errors[0]).to.have.property('details');", + " pm.expect(jsonData.errors[0].details).to.eql({ line: 1, column: 1 });", + "});", + "// The message is deliberately absent from details - it is already on the error itself,", + "// asserted above - so guard against it creeping back in.", + "pm.test(\"Details object does not repeat the message\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].details).to.not.have.property('message');", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 1\");", "});" ], "type": "text/javascript" @@ -551,6 +565,10 @@ "pm.test(\"Raw feedback from the xml2js parser should be displayed in the error message\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.map(obj => (obj.message))).to.include(\"Start tag expected, '<' not found\\n\");", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 1\");", "});" ], "type": "text/javascript" @@ -626,6 +644,10 @@ "pm.test(\"Summary object should have 1 critical error\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.summary.critical).to.eq(1);", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 4\");", "});" ], "type": "text/javascript" @@ -1227,6 +1249,87 @@ }, "response": [] }, + { + "name": "iati-org-value-sign-only - expect 0.3.1", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "// Regression test for js-validator-api#574. A sign-only xs:decimal", + "// (\" - \") was accepted by the old bundled libxml2 (2.9.9),", + "// so the file validated clean while the IATI Dashboard reported it as failing.", + "pm.test(\"Status code is 422\", function () { pm.response.to.have.status(422); });", + "pm.test(\"The file is not valid\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.valid).to.eql(false);", + "});", + "pm.test(\"fileType value is iati-organisations\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.fileType).to.eql('iati-organisations');", + "});", + "pm.test(\"version is 2.03\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.iatiVersion).to.eql('2.03');", + "});", + "pm.test(\"Errors include schema error id 0.3.1\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors.map(obj => (obj.id))).to.include('0.3.1');", + "});", + "pm.test(\"The sign-only decimal is named in the error context\", function () {", + " pm.expect(pm.response.text()).to.include(\"is not a valid value of the atomic type 'xs:decimal'\");", + "});", + "pm.test(\"Summary object should have 1 critical error\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.summary.critical).to.eq(1);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "followRedirects": false, + "followOriginalHttpMethod": false, + "followAuthorizationHeader": false + }, + "request": { + "method": "POST", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/xml" + } + ], + "body": { + "mode": "file", + "file": { + "src": "iati-org-value-sign-only.xml" + } + }, + "url": { + "raw": "{{baseURL}}/pub/validate?group=false", + "host": [ + "{{baseURL}}" + ], + "path": [ + "pub", + "validate" + ], + "query": [ + { + "key": "group", + "value": "false" + } + ] + } + }, + "response": [] + }, { "name": "iati-act-root-only - expect 0.3.1", "event": [ @@ -1390,6 +1493,10 @@ "pm.test(\"Raw feedback from the xml2js parser should be displayed in the error message\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.map(obj => (obj.message))).to.include(\"Opening and ending tag mismatch: reporting-org line 5 and not-reporting-org\\n\");", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 7\");", "});" ], "type": "text/javascript" @@ -1457,6 +1564,10 @@ "pm.test(\"Raw feedback from the xml2js parser should be displayed in the error message\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.map(obj => (obj.message))).to.include(\"Opening and ending tag mismatch: reporting-org line 13 and not-reporting-org\\n\");", + "});", + "pm.test(\"Error context reports the line of the parse failure\", function () {", + " var jsonData = pm.response.json();", + " pm.expect(jsonData.errors[0].context[0].text).to.eql(\"At line: 15\");", "});" ], "type": "text/javascript" diff --git a/integration-tests/test-files/iati-org-value-sign-only.xml b/integration-tests/test-files/iati-org-value-sign-only.xml new file mode 100644 index 00000000..5144e64a --- /dev/null +++ b/integration-tests/test-files/iati-org-value-sign-only.xml @@ -0,0 +1,17 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + + + - + + + diff --git a/package-lock.json b/package-lock.json index 45960800..e9f88a78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "js-validator-api", - "version": "2.4.3", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "js-validator-api", - "version": "2.4.3", + "version": "2.5.0", "hasInstallScript": true, "license": "AGPL-3.0-or-later", "dependencies": { @@ -14,7 +14,7 @@ "applicationinsights": "^2.7.3", "date-fns": "^2.30.0", "dotenv": "^16.3.1", - "libxmljs2": "^0.32.0", + "libxml2-wasm": "0.7.1", "node-fetch": "^3.3.2", "redis": "^4.4.0", "underscore": "^1.13.6", @@ -241,58 +241,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "dependencies": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "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/@mapbox/node-pre-gyp/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/@mapbox/node-pre-gyp/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@microsoft/applicationinsights-web-snippet": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", @@ -527,11 +475,6 @@ "node": ">=10.0.0" } }, - "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.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -627,6 +570,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -687,23 +631,6 @@ } } }, - "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", @@ -869,7 +796,8 @@ "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -880,18 +808,11 @@ "node": ">=8" } }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1058,14 +979,6 @@ "fsevents": "~2.3.2" } }, - "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/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -1202,14 +1115,6 @@ "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/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1239,7 +1144,8 @@ "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=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "node_modules/confusing-browser-globals": { "version": "1.0.10", @@ -1247,11 +1153,6 @@ "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", "dev": true }, - "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": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "node_modules/continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", @@ -1368,19 +1269,6 @@ "node": ">=0.4.0" } }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "node_modules/detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", - "engines": { - "node": ">=8" - } - }, "node_modules/diagnostic-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", @@ -1460,7 +1348,8 @@ "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==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/es-abstract": { "version": "1.22.1", @@ -2033,11 +1922,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -2111,21 +1995,11 @@ "node": ">=12.20.0" } }, - "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=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "node_modules/fsevents": { "version": "2.3.2", @@ -2173,25 +2047,6 @@ "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/generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -2265,6 +2120,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2411,11 +2267,6 @@ "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": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -2523,6 +2374,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2531,7 +2383,8 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "node_modules/internal-slot": { "version": "1.0.5", @@ -2646,6 +2499,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -2887,18 +2741,13 @@ "node": ">= 0.8.0" } }, - "node_modules/libxmljs2": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.32.0.tgz", - "integrity": "sha512-DuvKfSQZeUzw0A4UWZXfcBpr3VqlcJY1b3aw99PxTiX3T5t1rEO4gSpobNrP9S74LIhyDKaAs/lphuErV+n+7w==", - "hasInstallScript": true, - "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.10", - "bindings": "~1.5.0", - "nan": "~2.17.0" - }, + "node_modules/libxml2-wasm": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/libxml2-wasm/-/libxml2-wasm-0.7.1.tgz", + "integrity": "sha512-aZpJJL/j6T3D+5TmhG4D0ylR3mN6UzmqmBjyb/p+zEAaouG6GpfHiUNUzKR3vKCEoJt/Z2L15XPDCVPuFJIQhg==", + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/lilconfig": { @@ -3169,28 +3018,6 @@ "node": ">=10" } }, - "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.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3245,6 +3072,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3261,40 +3089,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "dependencies": { - "yallist": "^4.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/mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -3478,11 +3272,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" - }, "node_modules/nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -3536,20 +3325,6 @@ "url": "https://opencollective.com/node-fetch" } }, - "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-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3586,25 +3361,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -3705,6 +3461,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "dependencies": { "wrappy": "1" } @@ -3757,6 +3514,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -3870,19 +3628,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4043,6 +3788,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -4095,6 +3841,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -4146,11 +3893,6 @@ "randombytes": "^2.1.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4194,7 +3936,8 @@ "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==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/slice-ansi": { "version": "5.0.0", @@ -4229,14 +3972,6 @@ "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" }, - "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-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -4250,6 +3985,7 @@ "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", @@ -4308,6 +4044,7 @@ "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" }, @@ -4356,30 +4093,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "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" - }, - "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==", - "engines": { - "node": ">=8" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4398,11 +4111,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -4547,11 +4255,6 @@ "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": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -4568,20 +4271,6 @@ "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "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", @@ -4632,14 +4321,6 @@ "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/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -4675,7 +4356,8 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "node_modules/xml2js": { "version": "0.6.2", @@ -4932,40 +4614,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@mapbox/node-pre-gyp": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", - "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", - "requires": { - "detect-libc": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", - "nopt": "^5.0.0", - "npmlog": "^5.0.1", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.11" - }, - "dependencies": { - "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" - } - }, - "semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, "@microsoft/applicationinsights-web-snippet": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-web-snippet/-/applicationinsights-web-snippet-1.0.1.tgz", @@ -5135,11 +4783,6 @@ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==" }, - "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.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -5204,7 +4847,8 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "6.2.1", @@ -5242,20 +4886,6 @@ "diagnostic-channel-publishers": "1.0.7" } }, - "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", @@ -5373,7 +5003,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "binary-extensions": { "version": "2.2.0", @@ -5381,18 +5012,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5517,11 +5141,6 @@ "readdirp": "~3.6.0" } }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" - }, "cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -5621,11 +5240,6 @@ "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==" - }, "colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -5649,7 +5263,8 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "confusing-browser-globals": { "version": "1.0.10", @@ -5657,11 +5272,6 @@ "integrity": "sha512-gNld/3lySHwuhaVluJUKLePYirM3QNCKzVxqAdhJII9/WXKVX5PURzMVJspS1jTslSqjeuG4KMVTSouit5YPHA==", "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" - }, "continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", @@ -5739,16 +5349,6 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" - }, - "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" - }, "diagnostic-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-1.1.1.tgz", @@ -5810,7 +5410,8 @@ "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==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "es-abstract": { "version": "1.22.1", @@ -6246,11 +5847,6 @@ "flat-cache": "^3.0.4" } }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -6309,18 +5905,11 @@ "fetch-blob": "^3.1.2" } }, - "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=" + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true }, "fsevents": { "version": "2.3.2", @@ -6352,22 +5941,6 @@ "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" - } - }, "generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -6417,6 +5990,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -6512,11 +6086,6 @@ "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": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" - }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -6591,6 +6160,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -6599,7 +6169,8 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true }, "internal-slot": { "version": "1.0.5", @@ -6680,7 +6251,8 @@ "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==" + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true }, "is-glob": { "version": "4.0.3", @@ -6847,15 +6419,10 @@ "type-check": "~0.4.0" } }, - "libxmljs2": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/libxmljs2/-/libxmljs2-0.32.0.tgz", - "integrity": "sha512-DuvKfSQZeUzw0A4UWZXfcBpr3VqlcJY1b3aw99PxTiX3T5t1rEO4gSpobNrP9S74LIhyDKaAs/lphuErV+n+7w==", - "requires": { - "@mapbox/node-pre-gyp": "^1.0.10", - "bindings": "~1.5.0", - "nan": "~2.17.0" - } + "libxml2-wasm": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/libxml2-wasm/-/libxml2-wasm-0.7.1.tgz", + "integrity": "sha512-aZpJJL/j6T3D+5TmhG4D0ylR3mN6UzmqmBjyb/p+zEAaouG6GpfHiUNUzKR3vKCEoJt/Z2L15XPDCVPuFJIQhg==" }, "lilconfig": { "version": "2.1.0", @@ -7039,21 +6606,6 @@ "yallist": "^4.0.0" } }, - "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.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" - } - } - }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7093,6 +6645,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -7103,28 +6656,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, - "minipass": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.4.tgz", - "integrity": "sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==", - "requires": { - "yallist": "^4.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==" - }, "mocha": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", @@ -7254,11 +6785,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==" - }, "nanoid": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", @@ -7286,14 +6812,6 @@ "formdata-polyfill": "^4.0.10" } }, - "nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "requires": { - "abbrev": "1" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -7317,22 +6835,6 @@ } } }, - "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": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" - }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -7406,6 +6908,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1" } @@ -7445,7 +6948,8 @@ "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=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-key": { "version": "3.1.1", @@ -7509,16 +7013,6 @@ "safe-buffer": "^5.1.0" } }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -7638,6 +7132,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, "requires": { "glob": "^7.1.3" } @@ -7666,7 +7161,8 @@ "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==" + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true }, "safe-regex-test": { "version": "1.0.0", @@ -7698,11 +7194,6 @@ "randombytes": "^2.1.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7737,7 +7228,8 @@ "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==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "slice-ansi": { "version": "5.0.0", @@ -7762,14 +7254,6 @@ "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" }, - "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-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -7780,6 +7264,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7823,6 +7308,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -7850,26 +7336,6 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, - "tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "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" - }, - "dependencies": { - "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" - } - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -7885,11 +7351,6 @@ "is-number": "^7.0.0" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -8001,11 +7462,6 @@ "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" - }, "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -8016,20 +7472,6 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "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", @@ -8065,14 +7507,6 @@ "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" - } - }, "workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", @@ -8104,7 +7538,8 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "xml2js": { "version": "0.6.2", diff --git a/package.json b/package.json index 544a2cc6..ac8f5802 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "js-validator-api", - "version": "2.4.3", + "version": "2.5.0", "description": "JavaScript IATI Validator api", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { "start": "func start --javascript", "start:verbose": "func start --javascript --verbose", - "rules:test": "mocha './ruleset-unit-tests/**/test.js'", + "unit:test": "mocha './ruleset-unit-tests/**/test.js' './schema-unit-tests/**/test.js' './utils-unit-tests/**/test.js'", "int:test": "newman run integration-tests/js-validator-api-tests.postman_collection.json -e integration-tests/envs/local-function-7071.postman_environment.json --working-dir integration-tests/test-files", "docker:start": "docker compose up --build", "docker:int:test": "newman run integration-tests/js-validator-api-tests.postman_collection.json -e integration-tests/envs/local-docker-function.postman_environment.json --working-dir integration-tests/test-files", @@ -20,7 +20,7 @@ "applicationinsights": "^2.7.3", "date-fns": "^2.30.0", "dotenv": "^16.3.1", - "libxmljs2": "^0.32.0", + "libxml2-wasm": "0.7.1", "node-fetch": "^3.3.2", "redis": "^4.4.0", "underscore": "^1.13.6", diff --git a/schema-unit-tests/test-files/act_value_sign_only.xml b/schema-unit-tests/test-files/act_value_sign_only.xml new file mode 100644 index 00000000..3d5a5b71 --- /dev/null +++ b/schema-unit-tests/test-files/act_value_sign_only.xml @@ -0,0 +1,23 @@ + + + + AA-AAA-123456789-ABC123 + + Example narrative + + + <narrative>Example narrative</narrative> + + + Example narrative + + + + + + + + - + + + diff --git a/schema-unit-tests/test-files/org_bad_datetime.xml b/schema-unit-tests/test-files/org_bad_datetime.xml new file mode 100644 index 00000000..b52315ac --- /dev/null +++ b/schema-unit-tests/test-files/org_bad_datetime.xml @@ -0,0 +1,12 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + diff --git a/schema-unit-tests/test-files/org_undeclared_ns_prefix.xml b/schema-unit-tests/test-files/org_undeclared_ns_prefix.xml new file mode 100644 index 00000000..4c486e38 --- /dev/null +++ b/schema-unit-tests/test-files/org_undeclared_ns_prefix.xml @@ -0,0 +1,13 @@ + + + + AA-AAA-123456789 + + Organisation name + Undeclared prefix - not fatal to building the tree + + + Organisation name + + + diff --git a/schema-unit-tests/test-files/org_valid.xml b/schema-unit-tests/test-files/org_valid.xml new file mode 100644 index 00000000..c6e2ebd3 --- /dev/null +++ b/schema-unit-tests/test-files/org_valid.xml @@ -0,0 +1,17 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + + + 1000.00 + + + diff --git a/schema-unit-tests/test-files/org_value_long_decimal.xml b/schema-unit-tests/test-files/org_value_long_decimal.xml new file mode 100644 index 00000000..d5716c4b --- /dev/null +++ b/schema-unit-tests/test-files/org_value_long_decimal.xml @@ -0,0 +1,17 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + + + 111111111111111111111111111111 + + + diff --git a/schema-unit-tests/test-files/org_value_plus_only.xml b/schema-unit-tests/test-files/org_value_plus_only.xml new file mode 100644 index 00000000..5f05d56d --- /dev/null +++ b/schema-unit-tests/test-files/org_value_plus_only.xml @@ -0,0 +1,17 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + + + + + + + diff --git a/schema-unit-tests/test-files/org_value_sign_only.xml b/schema-unit-tests/test-files/org_value_sign_only.xml new file mode 100644 index 00000000..5144e64a --- /dev/null +++ b/schema-unit-tests/test-files/org_value_sign_only.xml @@ -0,0 +1,17 @@ + + + + AA-AAA-123456789 + + Organisation name + + + Organisation name + + + + + - + + + diff --git a/schema-unit-tests/test-files/org_value_sign_only_multiple.xml b/schema-unit-tests/test-files/org_value_sign_only_multiple.xml new file mode 100644 index 00000000..264ff8db --- /dev/null +++ b/schema-unit-tests/test-files/org_value_sign_only_multiple.xml @@ -0,0 +1,1168 @@ + + + + AA-AAA-123456789 + + Example narrative + Example narrative + + + Example narrative + Example narrative + + + + 1000.00 + + + 1000.00 + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + - + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + - + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + - + + + + 1000.00 + + + + - + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + 1000.00 + + + + - + + + + 1000.00 + + + + 1000.00 + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + <narrative>Example narrative</narrative> + + + + + + \ No newline at end of file diff --git a/schema-unit-tests/test.js b/schema-unit-tests/test.js new file mode 100644 index 00000000..7ce4ba75 --- /dev/null +++ b/schema-unit-tests/test.js @@ -0,0 +1,225 @@ +import fs from 'fs/promises'; +import { fileURLToPath } from 'url'; +// eslint-disable-next-line import/no-extraneous-dependencies +import chai from 'chai'; +import { XmlDocument, XsdValidator, XmlValidateError } from 'libxml2-wasm'; +import { xmlRegisterFsInputProviders } from 'libxml2-wasm/lib/nodejs.mjs'; + +import { validateIATI } from '../services/rulesValidator.js'; + +const { expect } = chai; + +/* + * Schema validation tests. + * + * Tests developed to prior to replacing libxmljs2. See issue: Upgrade version + * of libxmljs2 used to bring in line with Dashboard validation + * https://github.com/IATI/js-validator-api/issues/574 + * + * This upgrade is necessary to fix a defect in libxml2's handling of xs:decimal + * highlighted by the following issues: + * https://github.com/IATI/validator-web/issues/1002 + * https://github.com/IATI/IATI-Dashboard/issues/845 + * + * These exercise the XSD schema path. Nothing in ruleset-unit-tests/ does: 20 of its 24 + * suites go through allRulesResult or testRuleset, which never touch a schema at all, + * and the two that do call validateIATI pass three arguments - leaving its 4th + * parameter (schema) undefined, so the `if (schema)` guards short-circuit. A schema + * regression has therefore been invisible to PR CI. + * + * Picked up by the `npm run unit:test` glob, so PR CI runs it with everything else. + * + * Only 2.03 fixtures are validated, though config.VERSIONS is 2.01, 2.02 and 2.03. The + * defect is in libxml2's handling of xs:decimal, an XSD built-in type, so it behaves the + * same whichever IATI schema references it - checked against all three. What does differ + * between versions is the schema content itself, so the Schema compilation block builds + * all six rather than assuming 2.03 speaks for the rest. + */ + +const DECIMAL_ERROR = "is not a valid value of the atomic type 'xs:decimal'"; +const DATETIME_ERROR = "is not a valid value of the atomic type 'xs:dateTime'"; + +const testMap = [ + { + file: 'org_value_sign_only.xml', + fileType: 'iati-organisations', + what: 'a sign-only decimal " - " is rejected (the defect reported in #1002)', + occurrences: 1, + message: DECIMAL_ERROR, + }, + { + file: 'org_value_plus_only.xml', + fileType: 'iati-organisations', + what: 'a sign-only decimal " + " is rejected', + occurrences: 1, + message: DECIMAL_ERROR, + }, + { + file: 'act_value_sign_only.xml', + fileType: 'iati-activities', + what: 'a sign-only decimal is rejected in an activity file too', + occurrences: 1, + message: DECIMAL_ERROR, + }, + { + file: 'org_value_long_decimal.xml', + fileType: 'iati-organisations', + what: 'a 30-digit decimal is accepted (the inverse defect - the old parser rejects it)', + occurrences: 0, + }, + { + // Anonymised copy of the real file that triggered #1002. Adds what the minimal + // fixtures don't: five occurrences of one defect, which validateSchema groups + // by message into a single error carrying five contexts. + file: 'org_value_sign_only_multiple.xml', + fileType: 'iati-organisations', + what: 'repeated sign-only decimals are all reported, with their line numbers', + occurrences: 5, + // One below the file's actual lines (89, 125, 149, 157, 245). validateIATI + // validates each organisation as its own re-wrapped document and reconstructs + // line numbers by offset arithmetic, which lands one low for this fixture. + lines: [88, 124, 148, 156, 244], + message: DECIMAL_ERROR, + }, + { + // The libraries disagree on when a parse failure is fatal: libxmljs2 threw only + // if no document could be built, libxml2-wasm throws whenever the parser + // recorded anything at all. An undeclared namespace prefix is not fatal to + // building a tree, so it must stay a schema error here rather than becoming a + // file-level parse error. See utils/xmlParse.js, which restores the old rule. + file: 'org_undeclared_ns_prefix.xml', + fileType: 'iati-organisations', + what: 'an undeclared namespace prefix is a schema error, not a parse failure', + occurrences: 1, + message: 'This element is not expected', + }, + { + // Positive control. Guards against the suite going green by not validating at + // all - validateIATI silently skips schema work when `schema` is undefined. + file: 'org_bad_datetime.xml', + fileType: 'iati-organisations', + what: 'CONTROL: an invalid xs:dateTime is still caught', + occurrences: 1, + message: DATETIME_ERROR, + }, + { + // Negative control. + file: 'org_valid.xml', + fileType: 'iati-organisations', + what: 'CONTROL: a valid file reports nothing', + occurrences: 0, + }, +]; + +// Lets xsd:include / xsd:import resolve from disk. utils.js registers these too, under a +// comment saying it must happen once per process - which holds because this suite +// deliberately does not import utils.js, that being what keeps it free of Redis. If the +// two are ever loaded together, one of these calls has to go. +xmlRegisterFsInputProviders(); + +/* + * Reads an XSD and returns a compiled XsdValidator, mirroring what utils.js getSchema + * now caches. Unlike libxmljs2, compilation happens here rather than being deferred to + * validate(), so an unresolvable xsd:include throws at this point. + */ +const loadSchema = async (fileType, version) => { + const xsdPath = fileURLToPath( + new URL(`../schemas/${version}/${fileType}-schema.xsd`, import.meta.url), + ); + const xsdDoc = XmlDocument.fromBuffer(await fs.readFile(xsdPath), { url: xsdPath }); + try { + return XsdValidator.fromDoc(xsdDoc); + } finally { + xsdDoc.dispose(); + } +}; + +/* + * Retained from the libxmljs2 version, where loadSchema could return a document whose + * includes had silently failed to resolve. libxml2-wasm throws during compilation + * instead, so this is now belt-and-braces - it still fails if a schema builds but + * cannot validate. + */ +const assertSchemaUsable = (schema, fileType, version) => { + const probe = XmlDocument.fromString(`<${fileType} version="${version}" />`); + try { + expect(() => schema.validate(probe)).to.throw(XmlValidateError); + } finally { + probe.dispose(); + } +}; + +// Rule evaluation is not under test here, so an empty ruleset and empty id sets. +const emptyRuleset = {}; +const idSets = { 'ORG-ID': new Set(), 'ORG-ID-PREFIX': new Set() }; + +const runSchemaValidation = async (file, fileType, version) => { + const xml = (await fs.readFile(new URL(`./test-files/${file}`, import.meta.url))).toString(); + const schema = await loadSchema(fileType, version); + const { schemaErrors } = await validateIATI( + emptyRuleset, + xml, + fileType, + idSets, + schema, + false, + false, + ); + return schemaErrors; +}; + +// Errors sharing a message are grouped into one object carrying several contexts, +// so count contexts rather than error objects. +const countOccurrences = (schemaErrors) => + schemaErrors.reduce((total, error) => total + error.context.length, 0); + +const reportedLines = (schemaErrors) => + schemaErrors + .flatMap((error) => error.context.map((context) => context.text)) + .map((text) => { + const match = /At line(?: greater than)?: (\d+)/.exec(text); + return match ? Number(match[1]) : null; + }) + .filter((line) => line !== null); + +describe('Schema compilation', () => { + // getSchema loads six schemas: three versions times two file types. Each pulls in + // iati-common.xsd and xml.xsd, resolved relative to the baseUrl given at compile + // time. The mechanism is identical for all six, but the content is not - + // iati-common.xsd differs per version - so build every one. A schema that fails to + // build is not obvious: see assertSchemaUsable. + ['2.01', '2.02', '2.03'].forEach((version) => { + ['iati-activities', 'iati-organisations'].forEach((fileType) => { + it(`${fileType} ${version} compiles, resolving its includes`, async () => { + const schema = await loadSchema(fileType, version); + assertSchemaUsable(schema, fileType, version); + }); + }); + }); +}); + +describe('Schema validation', () => { + testMap.forEach((test) => { + it(`${test.file}: ${test.what}`, async () => { + const schemaErrors = await runSchemaValidation(test.file, test.fileType, '2.03'); + + expect(countOccurrences(schemaErrors)).to.equal(test.occurrences); + + schemaErrors.forEach((error) => { + expect(error.id).to.equal('0.3.1'); + expect(error.severity).to.equal('critical'); + expect(error.category).to.equal('schema'); + }); + + if (test.message) { + schemaErrors.forEach((error) => { + expect(error.message).to.contain(test.message); + }); + } + + if (test.lines) { + expect(reportedLines(schemaErrors)).to.have.members(test.lines); + } + }); + }); +}); diff --git a/services/rulesValidator.js b/services/rulesValidator.js index 7b0826ca..ace18c79 100644 --- a/services/rulesValidator.js +++ b/services/rulesValidator.js @@ -2,11 +2,12 @@ import { DOMParser } from '@xmldom/xmldom'; import xpath from 'xpath'; import _ from 'underscore'; import { compareAsc, differenceInDays } from 'date-fns'; -import libxml from 'libxmljs2'; +import { XmlValidateError } from 'libxml2-wasm'; import { Readable, Transform } from 'stream'; import { pipeline } from 'stream/promises'; import ruleNameObj from './ruleNameMap.js'; +import { parseXmlString } from '../utils/xmlParse.js'; const select = xpath.useNamespaces({ xml: 'http://www.w3.org/XML/1998/namespace' }); @@ -698,65 +699,85 @@ const standardiseResultFormat = (result, showDetails, xml, lineCount) => { }; }; +const toReportedDetail = ({ col }, line) => ({ line, column: col }); + +/* + * Turns validation into a value. libxml2-wasm throws XmlValidateError where libxmljs2 + * returned false, and the compiled XsdValidator is the receiver rather than the document. + * Confined to here so the grouping below is not nested inside a catch. Anything that is + * not a validation failure is rethrown - a fault in the validator itself must not be + * reported as a schema error in the file. + */ +const collectSchemaErrors = (schema, xmlDoc) => { + try { + schema.validate(xmlDoc); + return []; + } catch (error) { + if (!(error instanceof XmlValidateError)) throw error; + return error.details; + } +}; + const validateSchema = (xmlString, schema, identifier, title, showDetails, lineOffset = 0) => { - let xmlDoc = null; + let xmlDoc; try { - xmlDoc = libxml.parseXml(xmlString); + xmlDoc = parseXmlString(xmlString); } catch (error) { + const line = error.details?.[0]?.line; return [ { id: '0.3.1', category: 'schema', severity: 'critical', message: error.message, - context: [ - { text: `At line ${lineOffset + (error.line === undefined ? 0 : error.line)}` }, - ], + context: [{ text: `At line ${lineOffset + (line === undefined ? 0 : line)}` }], }, ]; } - if (xmlDoc != null && !xmlDoc.validate(schema)) { - const curSchemaErrors = xmlDoc.validationErrors.reduce((acc, error) => { - let errContext; - const errorDetail = error; - if ('line' in errorDetail) { - const lineMax = errorDetail.line >= 65535; - if (lineOffset > 1) { - errorDetail.line += lineOffset; - } - errContext = `At line${lineMax ? ' greater than' : ''}: ${errorDetail.line}${ - lineMax - ? `. Note: The validator cannot display accurate line numbers for schema errors located at a line greater than ${errorDetail.line} for this activity.` - : '' - }`; - } - if (!_.has(acc, error.message)) { - acc[error.message] = { + try { + const curSchemaErrors = collectSchemaErrors(schema, xmlDoc).reduce((acc, detail) => { + // Note the threshold is tested against the raw line but the offset one is + // reported, which is how this has always behaved. + const lineMax = detail.line >= 65535; + const line = lineOffset > 1 ? detail.line + lineOffset : detail.line; + const errContext = `At line${lineMax ? ' greater than' : ''}: ${line}${ + lineMax + ? `. Note: The validator cannot display accurate line numbers for schema errors located at a line greater than ${line} for this activity.` + : '' + }`; + if (!_.has(acc, detail.message)) { + acc[detail.message] = { id: '0.3.1', category: 'schema', severity: 'critical', - message: error.message, + message: detail.message, context: [{ text: errContext }], - ...(showDetails && { details: [{ error: errorDetail }] }), + ...(showDetails && { details: [{ error: toReportedDetail(detail, line) }] }), identifier, title, }; } else { - acc[error.message] = { - ...acc[error.message], - context: [...acc[error.message].context, { text: errContext }], + acc[detail.message] = { + ...acc[detail.message], + context: [...acc[detail.message].context, { text: errContext }], ...(showDetails && { - details: [...acc[error.message].details, { error: errorDetail }], + details: [ + ...acc[detail.message].details, + { error: toReportedDetail(detail, line) }, + ], }), }; } return acc; }, {}); return Object.keys(curSchemaErrors).map((errGroup) => curSchemaErrors[errGroup]); + } finally { + // Off-heap wasm memory, and this runs once per activity - a missing dispose + // grows the heap for the whole length of a large file. + xmlDoc.dispose(); } - return []; }; const fileDefinition = { diff --git a/services/schemaValidator.js b/services/schemaValidator.js index c632cce8..c7270fe2 100644 --- a/services/schemaValidator.js +++ b/services/schemaValidator.js @@ -1,4 +1,6 @@ -import { validateXMLrecover, getFileInformation, getSchema } from '../utils/utils.js'; +import { XmlValidateError } from 'libxml2-wasm'; +import { validateXMLrecover, getSchema } from '../utils/utils.js'; +import { parseFileBody, getFileInformation } from '../utils/iatiFile.js'; // pvt-schema-validate-file-post // Schema Check on full file, happens before full validation to enable the safety valve functionality in unified platform @@ -48,26 +50,21 @@ const schemaValidateFile = async (context, req) => { } let fileType = ''; let version = ''; - let xmlDoc = ''; let isIati = ''; let supportedVersion = ''; + // Undefined rather than '', so the xmlDoc?.dispose() below is a genuine no-op when + // no document was produced. `''?.dispose()` would be a TypeError. + let xmlDoc; // Parse file and get metadata for further checks try { - ({ fileType, version, supportedVersion, xmlDoc, isIati } = getFileInformation(body)); + xmlDoc = parseFileBody(body); + ({ fileType, version, supportedVersion, isIati } = getFileInformation(xmlDoc)); } catch (error) { - context.res = { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: { - valid: false, - }, - }; - return; - } + // The parse may have succeeded and the inspection failed, so this path can own a + // document. Unparseable input is simply not valid here, hence 200 either way. + xmlDoc?.dispose(); - // IATI Check - if (!isIati) { context.res = { status: 200, headers: { 'Content-Type': 'application/json' }, @@ -78,19 +75,47 @@ const schemaValidateFile = async (context, req) => { return; } - // Version Check - if (!supportedVersion) { - context.res = { - status: 200, - headers: { 'Content-Type': 'application/json' }, - body: { - valid: false, - }, - }; - return; + // This scope owns xmlDoc from here: off-heap memory, so the finally below covers the + // two early returns as well as the schema check. See validationService for why that + // is deterministic rather than left to the library's FinalizationRegistry backstop. + let schemaValid; + try { + // IATI Check + if (!isIati) { + context.res = { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: { + valid: false, + }, + }; + return; + } + + // Version Check + if (!supportedVersion) { + context.res = { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: { + valid: false, + }, + }; + return; + } + + try { + getSchema(fileType, version).validate(xmlDoc); + schemaValid = true; + } catch (error) { + if (!(error instanceof XmlValidateError)) throw error; + schemaValid = false; + } + } finally { + xmlDoc?.dispose(); } - if (!xmlDoc.validate(getSchema(fileType, version))) { + if (!schemaValid) { context.res = { headers: { 'Content-Type': 'application/json' }, body: { valid: false }, diff --git a/services/validationService.js b/services/validationService.js index 652e8844..32d5228b 100644 --- a/services/validationService.js +++ b/services/validationService.js @@ -1,6 +1,6 @@ import _ from 'underscore'; +import { XmlValidateError } from 'libxml2-wasm'; import { - getFileInformation, getRuleset, getSchema, getIdSets, @@ -10,6 +10,7 @@ import { validateXMLrecover, getObjectWithPropertiesAsEnumerable, } from '../utils/utils.js'; +import { parseFileBody, getFileInformation } from '../utils/iatiFile.js'; import { client, getStartTime, getElapsedTime } from '../config/appInsights.js'; import validateCodelists from './codelistValidator.js'; import { validateIATI } from './rulesValidator.js'; @@ -189,21 +190,34 @@ export default async function validate(context, req) { }; } + /* + * This scope owns xmlDoc: it holds memory outside the JS heap, so every path leaving + * between here and the schema check below disposes it first. Three do - the two early + * exits and the schema check's own `finally`. + * + * It is freed at the schema check rather than at the end of the function on purpose: + * codelist, ruleset and advisory validation all re-read the body as a string and + * never touch this document, so holding it that long would keep tens of MiB alive + * for most of the request. That is why one try/finally cannot simply wrap the whole + * function. + */ let xmlDoc; try { + xmlDoc = parseFileBody(body); ({ fileType: state.fileType, version: state.iatiVersion, generatedDateTime: state.generatedDateTime, supportedVersion: state.supportedVersion, isIati: state.isIati, - xmlDoc, - } = getFileInformation(body)); + } = getFileInformation(xmlDoc)); } catch (error) { + xmlDoc?.dispose(); + let errContext; - const { str1, str2, str3, line, level, int1, domain, column } = error; + const [{ line, col } = {}] = error.details ?? []; if (line) { - errContext = `At line: ${error.line}`; + errContext = `At line: ${line}`; } const errors = [ @@ -214,7 +228,7 @@ export default async function validate(context, req) { message: error.message, context: [{ text: errContext }], ...(showDetails && { - details: { str1, str2, str3, line, level, int1, domain, column }, + details: { line, column: col }, }), identifier: 'file', title: 'File level errors', @@ -241,6 +255,9 @@ export default async function validate(context, req) { // Check the XML file root element is or // (it doesn't do anything more than that) if (!state.isIati) { + // Per the contract on the declaration above: dispose before returning. + xmlDoc?.dispose(); + const errors = [ { id: '0.2.1', @@ -269,6 +286,8 @@ export default async function validate(context, req) { // Check that the version is latest IATI version if (!state.supportedVersion) { + xmlDoc?.dispose(); + const errors = [ { id: '0.6.1', @@ -301,8 +320,15 @@ export default async function validate(context, req) { // Check the file validates against the IATI schema const fileSchemaStart = getStartTime(); - state.schemaErrorsPresent = !xmlDoc.validate(getSchema(state.fileType, state.iatiVersion)); - xmlDoc = null; + try { + getSchema(state.fileType, state.iatiVersion).validate(xmlDoc); + state.schemaErrorsPresent = false; + } catch (error) { + if (!(error instanceof XmlValidateError)) throw error; + state.schemaErrorsPresent = true; + } finally { + xmlDoc?.dispose(); + } state.fileSchemaTime = getElapsedTime(fileSchemaStart); // Codelist Validation diff --git a/utils-unit-tests/test.js b/utils-unit-tests/test.js new file mode 100644 index 00000000..4831c287 --- /dev/null +++ b/utils-unit-tests/test.js @@ -0,0 +1,168 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import chai from 'chai'; +import { XmlParseError } from 'libxml2-wasm'; + +import { parseFileBody, getFileInformation } from '../utils/iatiFile.js'; + +const { expect } = chai; + +/* + * Tests for the first step of the validation pipeline: parse the body, then read what the + * pipeline needs off the resulting document. + * + * Fixtures are inline rather than in test-files/, unlike the sibling suites: every case + * here turns on one attribute of the root element, and a one-line string shows that more + * clearly than a file does. + * + */ + +const testMap = [ + { + what: 'the blank document xmllint --recover leaves behind yields no document', + body: '\n', + expected: { + fileType: '', + version: '', + generatedDateTime: '', + supportedVersion: undefined, + isIati: undefined, + }, + expectDocument: false, + }, + { + what: 'an iati-activities file reports its type, version and generated-datetime', + body: '', + expected: { + fileType: 'iati-activities', + version: '2.03', + generatedDateTime: '2026-01-01T00:00:00Z', + supportedVersion: true, + isIati: true, + }, + }, + { + what: 'an iati-organisations file is recognised too', + body: '', + expected: { + fileType: 'iati-organisations', + version: '2.03', + generatedDateTime: '', + supportedVersion: true, + isIati: true, + }, + }, + { + what: 'an absent version attribute is reported as empty, not thrown on', + // The guard behind this is `!= null`, not `!== undefined`: libxmljs2 returned + // undefined for a missing attribute where libxml2-wasm returns null, so the + // stricter check would fall through to .value and throw - surfacing as a bogus + // file-level 0.1.1 "not valid XML" instead of an unsupported-version report. + body: '', + expected: { + fileType: 'iati-activities', + version: '', + generatedDateTime: '', + supportedVersion: '', + isIati: true, + }, + }, + { + what: 'an absent generated-datetime is reported as empty', + body: '', + expected: { + fileType: 'iati-activities', + version: '2.03', + generatedDateTime: '', + supportedVersion: true, + isIati: true, + }, + }, + { + what: 'a version outside config.VERSIONS is flagged unsupported, driving error 0.6.1', + body: '', + expected: { + fileType: 'iati-activities', + version: '1.03', + generatedDateTime: '', + supportedVersion: false, + isIati: true, + }, + }, + { + what: 'a non-IATI root element is not IATI, and reports no file type', + body: '', + expected: { + fileType: '', + version: '2.03', + generatedDateTime: '', + supportedVersion: true, + isIati: false, + }, + }, + { + what: 'a version attribute on a nested element is picked up the same way', + body: '', + expected: { + fileType: '', + version: '2.03', + generatedDateTime: '', + supportedVersion: true, + isIati: false, + }, + }, +]; + +describe('parseFileBody / getFileInformation', () => { + testMap.forEach((test) => { + it(test.what, () => { + let xmlDoc; + try { + xmlDoc = parseFileBody(test.body); + if (test.expectDocument === false) { + expect(xmlDoc).to.equal(undefined); + } else { + expect(xmlDoc).to.not.equal(undefined); + } + expect(getFileInformation(xmlDoc)).to.deep.equal(test.expected); + } finally { + xmlDoc?.dispose(); + } + }); + }); + + it('throws on an unparseable body, carrying the position error 0.1.1 reports', () => { + let thrown; + try { + parseFileBody(''); + } catch (error) { + thrown = error; + } + + expect(thrown).to.be.instanceOf(XmlParseError); + expect(thrown.details).to.be.an('array').with.length.greaterThan(0); + expect(thrown.details[0].line).to.be.a('number'); + expect(thrown.details[0].col).to.be.a('number'); + expect(thrown.message).to.be.a('string').and.not.equal(''); + }); + + it('tolerates an undeclared namespace prefix rather than failing to parse', () => { + // libxml2-wasm throws whenever libxml2 recorded any error, even when a usable + // document was produced; libxmljs2 threw only when no document could be built. + // utils/xmlParse.js restores the older rule, and without it this file would be + // rejected as a file-level 0.1.1 instead of reaching schema validation. The + // schema-error side of this is covered by schema-unit-tests. + let xmlDoc; + try { + xmlDoc = parseFileBody( + '', + ); + expect(xmlDoc).to.not.equal(undefined); + const info = getFileInformation(xmlDoc); + expect(info.isIati).to.equal(true); + expect(info.fileType).to.equal('iati-activities'); + expect(info.version).to.equal('2.03'); + } finally { + xmlDoc?.dispose(); + } + }); +}); diff --git a/utils/iatiFile.js b/utils/iatiFile.js new file mode 100644 index 00000000..5a528e9a --- /dev/null +++ b/utils/iatiFile.js @@ -0,0 +1,69 @@ +import { ParseOption } from 'libxml2-wasm'; + +import config from '../config/config.js'; +import { parseXmlBuffer } from './xmlParse.js'; + +/* + * These two are a standalone module rather than part of utils.js so that they can be unit + * tested: importing utils.js pulls in config/redis.js, which connects at module load, and + * starts the fire-and-forget loader that fetches from GitHub. Same reason xmlParse.js is + * separate. + */ + +/* + * Parses the request body into a document for the caller to inspect and then dispose. The + * caller owns it: documents hold memory outside the JS heap, where libxmljs2's were on it. + * The library does register a FinalizationRegistry backstop, but its own docs say not to + * rely on it - so the caller is responsible for freeing deterministically. + * + * Throws for a body that genuinely cannot be parsed, which callers report as 0.1.1. The + * position they need is on `error.details[0]`, not on the error itself as libxmljs2 had it. + */ +const parseFileBody = (body) => + body.toString() === `\n` + ? undefined + : parseXmlBuffer(Buffer.from(body), ParseOption.XML_PARSE_HUGE); + +/* + * Reports what the validation pipeline needs in order to decide how to proceed: which IATI + * file type this is, which version it declares, and whether that version is one we support. + * + * Reads the document and owns nothing - disposal stays with whoever called parseFileBody. + * Accepts undefined so the blank-document case needs no special handling at the call site. + */ +const getFileInformation = (xmlDoc) => { + let fileType = ''; + let version = ''; + let generatedDateTime = ''; + let supportedVersion; + let isIati; + if (xmlDoc !== undefined) { + const root = xmlDoc.root.name; + + isIati = root === 'iati-activities' || root === 'iati-organisations'; + // set fileType to '' for non IATI files + fileType = isIati ? root : ''; + + // `!= null`, not `!== undefined`: libxmljs2 returned undefined for an absent + // attribute where libxml2-wasm returns null, so the stricter check would fall + // through to .value and throw. + const versionAttr = xmlDoc.get(`/${fileType}/@version`); + if (versionAttr != null) { + version = versionAttr.value; + } + const generatedAttr = xmlDoc.get(`/${fileType}/@generated-datetime`); + if (generatedAttr != null) { + generatedDateTime = generatedAttr.value; + } + supportedVersion = version && config.VERSIONS.includes(version); + } + return { + fileType, + version, + generatedDateTime, + supportedVersion, + isIati, + }; +}; + +export { parseFileBody, getFileInformation }; diff --git a/utils/utils.js b/utils/utils.js index 492ff7f8..4c7704d8 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -1,4 +1,5 @@ -import libxml from 'libxmljs2'; +import { XmlDocument, XsdValidator } from 'libxml2-wasm'; +import { xmlRegisterFsInputProviders } from 'libxml2-wasm/lib/nodejs.mjs'; import fs from 'fs/promises'; import fetch from 'node-fetch'; import { spawn } from 'child_process'; @@ -6,18 +7,29 @@ import { spawn } from 'child_process'; import redisclient from '../config/redis.js'; import config from '../config/config.js'; -const GITHUB_RAW = 'https://raw.githubusercontent.com'; +// Lets libxml2 resolve xsd:include / xsd:import from disk when compiling the schemas +// below. Must be called exactly once per process: repeated register/cleanup leaks +// entries from the wasm function table (libxml2-wasm issue #166). +xmlRegisterFsInputProviders(); + const GITHUB_API = 'https://api.github.com'; const getFileBySha = async (owner, repo, sha, filePath) => { - // https://raw.githubusercontent.com/IATI/IATI-Validator-Codelists/{sha}/codelist_rules.json - const res = await fetch(`${GITHUB_RAW}/${owner}/${repo}/${sha}/${filePath}`, { - method: 'GET', - headers: { - Accept: 'text/plain', - Authorization: `token ${config.BASIC_GITHUB_TOKEN}`, + const headers = { Accept: 'application/vnd.github.raw+json' }; + if (config.GITHUB_OAUTH_APP_CLIENT_ID && config.GITHUB_OAUTH_APP_CLIENT_SECRET) { + headers.Authorization = `Basic ${Buffer.from( + `${config.GITHUB_OAUTH_APP_CLIENT_ID}:${config.GITHUB_OAUTH_APP_CLIENT_SECRET}`, + ).toString('base64')}`; + } + const res = await fetch( + `${GITHUB_API}/repos/${owner}/${repo}/contents/${filePath}?ref=${sha}`, + { + method: 'GET', + headers, }, - }); + ); + // This can be useful to check auth. You should see headers like x-ratelimit-limit, x-ratelimit-remaining + // console.log(res.headers); const body = res.json(); if (res.status !== 200) throw new Error( @@ -27,35 +39,25 @@ const getFileBySha = async (owner, repo, sha, filePath) => { }; const getFileCommitSha = async (owner, repo, branch, filePath) => { - // https://api.github.com/repos/IATI/IATI-Validator-Codelists/branches/version-2.03 - const branchRes = await fetch(`${GITHUB_API}/repos/${owner}/${repo}/branches/${branch}`, { - method: 'GET', - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `token ${config.BASIC_GITHUB_TOKEN}`, - }, - }); - const branchBody = await branchRes.json(); - if (branchRes.status !== 200) - throw new Error( - `Error fetching sha from github api. Status: ${branchRes.status} Message: ${branchBody.message} `, - ); - const { sha } = branchBody.commit; - // https://api.github.com/repos/IATI/IATI-Validator-Codelists/commits?sha={sha}&path=codelist_rules.json + const headers = { Accept: 'application/vnd.github.v3+json' }; + if (config.GITHUB_OAUTH_APP_CLIENT_ID && config.GITHUB_OAUTH_APP_CLIENT_SECRET) { + headers.Authorization = `Basic ${Buffer.from( + `${config.GITHUB_OAUTH_APP_CLIENT_ID}:${config.GITHUB_OAUTH_APP_CLIENT_SECRET}`, + ).toString('base64')}`; + } const fileRes = await fetch( - `${GITHUB_API}/repos/${owner}/${repo}/commits?sha=${sha}&path=${filePath}`, + `${GITHUB_API}/repos/${owner}/${repo}/commits?sha=${branch}&path=${filePath}`, { method: 'GET', - headers: { - Accept: 'application/vnd.github.v3+json', - Authorization: `token ${config.BASIC_GITHUB_TOKEN}`, - }, + headers, }, ); + // This can be useful to check auth. You should see headers like x-ratelimit-limit, x-ratelimit-remaining + // console.log(fileRes.headers); const fileBody = await fileRes.json(); if (fileRes.status !== 200) throw new Error( - `Error fetching sha from github api. Status: ${branchRes.status} Message: ${fileBody.message} `, + `Error fetching sha from github api. Status: ${fileRes.status} Message: ${fileBody.message} `, ); // sort to get newest commit fileBody.sort( @@ -66,41 +68,6 @@ const getFileCommitSha = async (owner, repo, branch, filePath) => { }; // parse xml body to JSON to check the root element, don't attempt to parse if output from xmllint --recover was just blank XML doc -const getFileInformation = (body) => { - let fileType = ''; - let version = ''; - let generatedDateTime = ''; - let supportedVersion; - let isIati; - let xmlDoc; - if (body.toString() !== `\n`) { - xmlDoc = libxml.parseXml(body, { huge: true }); - if (xmlDoc) { - const root = xmlDoc.root().name(); - - isIati = root === 'iati-activities' || root === 'iati-organisations'; - // set fileType to '' for non IATI files - fileType = isIati ? root : ''; - - if (xmlDoc.get(`/${fileType}/@version`) !== undefined) { - version = xmlDoc.get(`/${fileType}/@version`).value(); - } - if (xmlDoc.get(`/${fileType}/@generated-datetime`) !== undefined) { - generatedDateTime = xmlDoc.get(`/${fileType}/@generated-datetime`).value(); - } - supportedVersion = version && config.VERSIONS.includes(version); - } - } - return { - fileType, - version, - generatedDateTime, - supportedVersion, - isIati, - xmlDoc, - }; -}; - const codelistRules = {}; const ruleset = {}; const schemas = {}; @@ -183,10 +150,20 @@ config.VERSIONS.forEach(async (version) => { // load schemas ['iati-activities', 'iati-organisations'].forEach(async (fileType) => { - schemas[`${fileType}-${version}`] = libxml.parseXml( - (await fs.readFile(`schemas/${version}/${fileType}-schema.xsd`)).toString(), - { baseUrl: `./schemas/${version}/` }, - ); + try { + const path = `schemas/${version}/${fileType}-schema.xsd`; + // Parsed strictly rather than through xmlParse.js - a malformed XSD must fail + // here rather than be tolerated. + const xsdDoc = XmlDocument.fromBuffer(await fs.readFile(path), { url: path }); + try { + schemas[`${fileType}-${version}`] = XsdValidator.fromDoc(xsdDoc); + } finally { + // The validator retains what it needs; the source document does not. + xsdDoc.dispose(); + } + } catch (error) { + console.error(`Error loading ${fileType} schema for version ${version}: ${error}`); + } }); // load advisories @@ -244,6 +221,11 @@ const getRulesetCommitSha = (version) => { return ''; }; +/* + * Returns a compiled XsdValidator, not a parsed XSD document as it did under libxmljs2. + * Callers validate with `getSchema(...).validate(doc)`, which THROWS XmlValidateError on + * a schema-invalid document rather than returning false. + */ const getSchema = (fileType, version) => { if ( config.VERSIONS.includes(version) && @@ -469,7 +451,6 @@ export { getIdSets, getOrgIds, getOrgIdPrefixes, - getFileInformation, getVersionCodelistRules, getVersionCodelistCommitSha, getRuleset, diff --git a/utils/xmlParse.js b/utils/xmlParse.js new file mode 100644 index 00000000..b67e8198 --- /dev/null +++ b/utils/xmlParse.js @@ -0,0 +1,34 @@ +/* eslint-disable no-bitwise */ +import { XmlDocument, ParseOption } from 'libxml2-wasm'; + +/* + * libxml2-wasm throws whenever the parser recorded any error, even when it still built + * a usable document. libxmljs2 threw only when no document resulted, so problems that + * are not fatal to building a tree - an undeclared namespace prefix, for instance - + * used to parse through and be reported later as schema errors instead. + * + * Suppressing error collection restores that: with nothing collected, the throw depends + * on whether a document was produced, which is the libxmljs2 rule. Verified to agree + * with libxmljs2 on every integration fixture. + * + * The cost is that the resulting XmlParseError carries no message, line or column, and + * the 0.1.1 and 0.3.1 reports need those. So a failed parse is repeated with collection + * enabled purely to obtain the diagnostic. That second parse only happens for input that + * is genuinely unparseable, which the request is about to be rejected for anyway. + * + */ +const QUIET = ParseOption.XML_PARSE_NOERROR | ParseOption.XML_PARSE_NOWARNING; + +const parseTolerantly = (parse, source, option) => { + try { + return parse(source, { option: option | QUIET }); + } catch { + return parse(source, { option }); + } +}; + +export const parseXmlBuffer = (buffer, option = ParseOption.XML_PARSE_DEFAULT) => + parseTolerantly((source, options) => XmlDocument.fromBuffer(source, options), buffer, option); + +export const parseXmlString = (string, option = ParseOption.XML_PARSE_DEFAULT) => + parseTolerantly((source, options) => XmlDocument.fromString(source, options), string, option);