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
+
+
+ Example 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
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example narrative
+
+
+
+
+
+ Example 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);