From 1aae54135c826300996934ccc02f8194f2fa0d0e Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 22 Jul 2026 15:52:54 -0400 Subject: [PATCH 01/20] Remove Artillery test ownership --- .gitignore | 3 - README.md | 40 - package-lock.json | 8687 +++++------------------------------ package.json | 5 - test/artillery/.env.example | 6 - test/artillery/README.md | 96 - test/artillery/contract.yml | 138 - test/artillery/load.yml | 141 - test/artillery/smoke.yml | 128 - test/artillery/stress.yml | 151 - 10 files changed, 1085 insertions(+), 8310 deletions(-) delete mode 100644 test/artillery/.env.example delete mode 100644 test/artillery/README.md delete mode 100644 test/artillery/contract.yml delete mode 100644 test/artillery/load.yml delete mode 100644 test/artillery/smoke.yml delete mode 100644 test/artillery/stress.yml diff --git a/.gitignore b/.gitignore index e3a9605..5e19885 100644 --- a/.gitignore +++ b/.gitignore @@ -81,9 +81,6 @@ web_modules/ .env.test.local .env.production.local .env.local -test/artillery/.env -test/artillery/.env.* -!test/artillery/.env.example # parcel-bundler cache (https://parceljs.org/) .cache diff --git a/README.md b/README.md index 9ea51b4..45c1d67 100644 --- a/README.md +++ b/README.md @@ -199,17 +199,6 @@ npm run coverage npm run quality ``` -Artillery live API checks can be run against a running API and OpenSearch deployment: - -```bash -npm run artillery:smoke -npm run artillery:contract -npm run artillery:load -npm run artillery:stress -``` - -See `test/artillery/README.md` for environment selection, Artillery Cloud recording, and target override examples. - Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. The timing log is redacted and includes request strategy, cache hit status, in-flight hit status, candidate counts, filtered counts, returned count, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. ### CPE Post-Filter Cache @@ -248,35 +237,6 @@ This helps most when multiple users submit the same expensive CPE search within The cache does not share entries across ECS tasks. With multiple tasks, each task warms independently depending on load balancer routing. Steady repeated traffic can warm every active task, but unique one-off searches still pay the cold-cache cost. The per-process TTL, entry limit, cold-search concurrency, and cold-search queue depth can be tuned with `SearchPostFilterCacheTtlMs`, `SearchPostFilterCacheMaxEntries`, `SearchPostFilterColdSearchConcurrency`, and `SearchPostFilterColdSearchMaxQueueDepth`. Future scale improvements could add a shared cache such as Redis/ElastiCache. -### Interpreting Artillery Results - -Artillery exits with status code `0` when the profile finishes and all expectations and SLO checks pass. A non-zero exit means at least one request expectation, virtual user run, or `ensure` SLO check failed. - -Expectation failures come from request-level `expect` checks. These validate HTTP response behavior such as status codes, JSON content type, and required response properties. A failed expectation usually points to a route contract issue, an unavailable upstream dependency, or a test payload that no longer matches the API contract. - -SLO failures come from the `ensure` gates in each profile: - -- `smoke.yml`: p95 response time must stay under 5000 ms and virtual user error rate must be 0. -- `contract.yml`: p95 response time must stay under 5000 ms and virtual user error rate must be 0. -- `load.yml`: p95 response time must stay under 3000 ms, p99 response time must stay under 8000 ms, and virtual user error rate must be at or below 1%. -- `stress.yml`: p95 response time must stay under 8000 ms, p99 response time must stay under 15000 ms, and virtual user error rate must be at or below 5%. - -Latency percentiles summarize response time distribution. `p95` means 95% of measured responses completed at or below that value; `p99` means 99% completed at or below that value. A high p95 or p99 with successful status codes usually means the API is reachable but slower than the profile allows. - -Virtual user counters show whether Artillery completed the test flow. `vusers.created` is the number of virtual users started, and `vusers.completed` is the number that finished. A gap between those values contributes to the virtual user error rate used by `maxErrorRate`. - -Endpoint-level metrics are grouped by stable request names such as `POST /search keyword`, `POST /search cveIds`, and `POST /webSearch keyword`. Use these groups to identify whether a failure or latency spike is broad across the API or isolated to a specific request shape. - -When recording to Artillery Cloud, compare runs with the same `env` and `profile` tags. For example, compare `env:test,profile:smoke` runs to other test smoke runs rather than to local, load, or stress runs. Treat `stress.yml` failures as capacity signals rather than route contract failures unless the failing endpoint also fails smoke or contract checks. - -If Artillery fails with `SELF_SIGNED_CERT_IN_CHAIN` while recording to Artillery Cloud or calling an HTTPS deployment, configure Node with the same host CA bundle used for local Docker builds. `Dockerfile.local` receives that file through the `NPM_CA_CERT` environment variable and mounts it as a build secret at `/run/secrets/npm_ca`; local Artillery runs need the host path: - -```bash -export NODE_EXTRA_CA_CERTS="$NPM_CA_CERT" -``` - -Then rerun the Artillery command with `ARTILLERY_CLOUD_API_KEY`, `--record`, and the target environment variables. Do not commit Artillery Cloud keys, deployment URLs, or certificate file paths. - Docker runtime smoke checks can be run after building the image. The default `Dockerfile` avoids BuildKit `RUN --mount` syntax for AWS builders that do not support it: ```bash diff --git a/package-lock.json b/package-lock.json index 38feb69..258e05f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "artillery": "^2.0.33", "eslint": "^10.6.0", "globals": "^17.7.0", "jest": "^28.1.3", @@ -29,37 +28,70 @@ "swagger-autogen": "^2.23.7" } }, - "node_modules/@artilleryio/int-commons": { - "version": "2.24.0", - "resolved": "https://registry.npmjs.org/@artilleryio/int-commons/-/int-commons-2.24.0.tgz", - "integrity": "sha512-edCjaTcUQNWxGS7XD128zYV4wABnh4itGmK1bzRTUe78huYGN9d21/Sf/aHWgl/DbbufYvvvU1FX9PD1l0m7jg==", + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "async": "^2.6.4", - "cheerio": "^1.2.0", - "debug": "^4.4.3", - "deep-for-each": "^3.0.0", - "espree": "^10.3.0", - "jsonpath-plus": "^10.3.0", - "lodash": "^4.18.0", - "ms": "^2.1.3" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-commons/node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@artilleryio/int-commons/node_modules/debug": { + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/core/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", @@ -77,4910 +109,1125 @@ } } }, - "node_modules/@artilleryio/int-commons/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@artilleryio/int-commons/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@artilleryio/int-core": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/@artilleryio/int-core/-/int-core-2.28.0.tgz", - "integrity": "sha512-l1T2LMc0+BpGRH1sSfwb+jfeJWyTq0xrtJggRm3M7pMNU8Il1xZZBxxM2j+o7i8tXQPHZ/uSxdMUGNRaS6ygPA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@artilleryio/int-commons": "2.24.0", - "@artilleryio/sketches-js": "^2.1.1", - "agentkeepalive": "^4.6.0", - "arrivals": "^2.1.2", - "async": "^2.6.4", - "chalk": "^2.4.2", - "cheerio": "^1.2.0", - "cookie-parser": "^1.4.7", - "csv-parse": "^4.16.3", - "debug": "^4.4.3", - "decompress-response": "^6.0.0", - "deep-for-each": "^3.0.0", - "driftless": "^2.0.3", - "esprima": "^4.0.1", - "eventemitter3": "^5.0.4", - "fast-deep-equal": "^3.1.3", - "filtrex": "^0.5.4", - "form-data": "^4.0.5", - "got": "^14.6.6", - "hpagent": "^0.1.1", - "https-proxy-agent": "^5.0.0", - "lodash": "^4.18.0", - "ms": "^2.1.3", - "protobufjs": "^7.5.5", - "socket.io-client": "^4.8.3", - "socketio-wildcard": "^2.0.0", - "tough-cookie": "^5.1.2", - "uuid": "^11.1.0", - "ws": "^7.5.7" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@artilleryio/int-core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=6.0" + "node": ">=6.9.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@artilleryio/int-core/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/int-core/node_modules/hpagent": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz", - "integrity": "sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@artilleryio/int-core/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, "engines": { - "node": ">=4" + "node": ">=6.9.0" } }, - "node_modules/@artilleryio/sketches-js": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@artilleryio/sketches-js/-/sketches-js-2.1.1.tgz", - "integrity": "sha512-H3D50vDb37E3NGYXY0eUFAm5++moElaqoAu0MWYZhgzaA3IT2E67bRCL8U4LKHuVf/MgDZk14uawIjc4WVjOUQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/@aws-sdk/checksums": { - "version": "3.1000.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.16.tgz", - "integrity": "sha512-EKnvkXSmz3IpA99tCNuI+dLFXyZyClSm8zns9sB/elvkU+MTuomAs6toJMPMBf98/fICG/urXDkzGz0/c3yyAQ==", + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, + "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/checksums/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.1083.0.tgz", - "integrity": "sha512-k9n4XAYtl0QhQLQA9SGEl3hpqvTmlrAbPP44xJ4fzeZrYtRdRuvq5jTo8IcbId0VrPsBPB04DU6uJjulfgoQSA==", + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/middleware-compression": "^4.5.7", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1083.0.tgz", - "integrity": "sha512-rTfxzi7nhd7FXrdASPhenE0aZmrFMY8D1c8CUyE/L+FlfWrCO/e5LbFrxnUvrteY1KM+VUVWzZHjrmfHQW3upw==", + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.0.0" } }, - "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-ec2": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ec2/-/client-ec2-3.1083.0.tgz", - "integrity": "sha512-An9zV+dlsfwAcVOLwQWlfDHaH4eS8vWZqANn/sAshPqWZOs4a+wV4IMI+nezU2gBU65vDTJdwvSwN7OMT5RrXA==", + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/middleware-sdk-ec2": "^3.972.45", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-ec2/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-ecs": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ecs/-/client-ecs-3.1083.0.tgz", - "integrity": "sha512-BPEDMvMX58D46nc1aDehZ+zUzQ30/yxgDU0lA6N2lEgBYBYnZqA1PHmi/2LBcyGouf5cNL2ldmuNosWvjl/hDg==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-ecs/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-iam": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-iam/-/client-iam-3.1083.0.tgz", - "integrity": "sha512-1BfmzDtaHTt4zQqbeV134bly5xN0j6TaPBisb9K1iB7Arq2LdjD8sgEWxmM0qZeO/9B0enpngaWXHaF74gnPTQ==", - "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.12.13" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-iam/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-lambda": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.1083.0.tgz", - "integrity": "sha512-k6O+nOkNFy3LPpZ/GFD/admJMAhzdpxbZ6swdXoWjRltYAFVhXFdI6pdIWfai0gVA+xZ7MKM9sRTiPLBASnYiQ==", + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-lambda/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1083.0.tgz", - "integrity": "sha512-3YOicHy6qjexsy4rHk5jPvtLgg6Ho1u+ycOWtJcVTAIMlMq32MRtnhllAyFSezhAXeBnnLLtKtAGXim7vV8oog==", + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/checksums": "^3.1000.16", - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/middleware-sdk-s3": "^3.972.62", - "@aws-sdk/signature-v4-multi-region": "^3.996.39", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-sqs": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1083.0.tgz", - "integrity": "sha512-onW6oteUiZbVpGn2oMsPVk9bNKLLoYkIjzbmnNvIjPGvOTt5EWCW3huPWDfuKUiM69NkExWGqn0obmxfh52yqw==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/middleware-sdk-sqs": "^3.972.35", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-sqs/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-ssm": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1083.0.tgz", - "integrity": "sha512-1hfvo8G16GX6zk1JkzWIB9kFuoC/a9rOuucbxi1LgFuGa2gHLt8tAYmkLLlpBVMRLCZWcYR9FihyYA3jATMtEQ==", + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-ssm/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/client-sts": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1083.0.tgz", - "integrity": "sha512-3DRm8UNj3aByu0U5Sgvs5Ut3V1XbWiST/JWF3/Zk57kFeVYm4a/EjQH4k9jTI8pk4dmYkZZJMcaecqTRRYzYZg==", + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/signature-v4-multi-region": "^3.996.39", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/core": { - "version": "3.975.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.1.tgz", - "integrity": "sha512-8qh/6EYb7hl/ZwVfQufhbMEZs1gQIc7GbdrIf4eprQJ7cv042+74nE6l3YDfyWNzb9iPXb8fRyYSHkNIk5eE6Q==", + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/types": "^3.974.0", - "@aws-sdk/xml-builder": "^3.972.34", - "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.29.2", - "@smithy/signature-v4": "^5.6.3", - "@smithy/types": "^4.16.0", - "bowser": "^2.11.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/core/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.56", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.56.tgz", - "integrity": "sha512-nedaZz6Wz2FGUBMsWhlvBPGIkTMfRuMJ99YDHJI4CaC31JS8WPyTavEAio74cfd5nGhi0A8XASB4fZA5VqTdOQ==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-cognito-identity/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.57", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.57.tgz", - "integrity": "sha512-1RfJaF7SW1TOnvNGU7kaYjwUf5H3sfm+synGH1bHhRlqcnxCt3szebH3dmKEyY4tuGcbQ6ffzUT89cRitBV8OQ==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-env/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.59", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.59.tgz", - "integrity": "sha512-sRCkpTiFnCdQvuaRVjQ6SVoHu6i7RUpurVo1c4F81HWhPvUJ7Wdp5MNtSdX1O29CNXc8em3O5m52hCjVtAD9SA==", + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-http/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.1.tgz", - "integrity": "sha512-6d8H6ZAh3ZPKZ6fe1nG2OWeZEZPtt9ravoD1dezPdPtsSkJRoxGAnFSHwKT3E/Te6fHE30zRzjV6TD12rvF6yQ==", + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-env": "^3.972.57", - "@aws-sdk/credential-provider-http": "^3.972.59", - "@aws-sdk/credential-provider-login": "^3.972.63", - "@aws-sdk/credential-provider-process": "^3.972.57", - "@aws-sdk/credential-provider-sso": "^3.973.1", - "@aws-sdk/credential-provider-web-identity": "^3.972.63", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/credential-provider-imds": "^4.4.7", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-ini/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.63", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.63.tgz", - "integrity": "sha512-GREWRrMj0XnNKMaVa/Mauoaui26qBEHu71WWqXbwZOu/jFQOnPZjTf7u0KtGKC8VGa6VUs9kDWGgocrKNLS9vw==", + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-login/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.66", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.66.tgz", - "integrity": "sha512-f+qjRXZpz7sgzbc4QB+6nLKfyKFgRRXzWdXbsKPv/VhVRyHsDyq4yBWC/B75BAJpFIcUeI2XR/3gdWJ677zB4A==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.57", - "@aws-sdk/credential-provider-http": "^3.972.59", - "@aws-sdk/credential-provider-ini": "^3.973.1", - "@aws-sdk/credential-provider-process": "^3.972.57", - "@aws-sdk/credential-provider-sso": "^3.973.1", - "@aws-sdk/credential-provider-web-identity": "^3.972.63", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/credential-provider-imds": "^4.4.7", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-node/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.57", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.57.tgz", - "integrity": "sha512-TiVQhuU0pbhIZAUZacbPHMyzrIdiH+lnx+PMY/Pu/b93dJrq3wdZwzUJ0TPpvNxaqbHsxJvQZW3/h/beLiKq7Q==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@aws-sdk/credential-provider-process/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.1.tgz", - "integrity": "sha512-3foTZUJ4821Ij60X7K3NJroygiZLnbBmarN+T//O2cjkISan90zElN3NBmgSlDrTQ7Gs6z/yO8V7h60QNcDZHQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/token-providers": "3.1083.0", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "license": "MIT", "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.63", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.63.tgz", - "integrity": "sha512-8qZLFhM69eKcS37m459ctPR05Qimycm/74OPVioe6wNZabMT54GYhwBju0+J656RkMasNSawWQu+c8CmBe3TUQ==", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/credential-providers": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1083.0.tgz", - "integrity": "sha512-i2q3Jgt365lZp7BSDqDSf283WvISrXob1zsql093LK3G2svYRHRvcNv995SsKtAzRENwHem2TC2vWeghrgBkxg==", + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.56", - "@aws-sdk/credential-provider-env": "^3.972.57", - "@aws-sdk/credential-provider-http": "^3.972.59", - "@aws-sdk/credential-provider-ini": "^3.973.1", - "@aws-sdk/credential-provider-login": "^3.972.63", - "@aws-sdk/credential-provider-node": "^3.972.66", - "@aws-sdk/credential-provider-process": "^3.972.57", - "@aws-sdk/credential-provider-sso": "^3.973.1", - "@aws-sdk/credential-provider-web-identity": "^3.972.63", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/credential-provider-imds": "^4.4.7", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/credential-providers/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/middleware-sdk-ec2": { - "version": "3.972.45", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-ec2/-/middleware-sdk-ec2-3.972.45.tgz", - "integrity": "sha512-wcSIL3HWcEQQkf+eKbyhwqSML23uTUI2zAYexRRa0akL74pnq5olKh06a0kGHWsn6SUHcw9zy9dExA8UuJAUCw==", + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/signature-v4": "^5.6.3", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "ms": "^2.1.3" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@aws-sdk/middleware-sdk-ec2/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.62", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.62.tgz", - "integrity": "sha512-k8JJwYXVYlOOjWnPZDThQS1xDFJgi5Dokt73qFlDtrZAbdcint5aIdjB9XgJAAQVP5OoqcefQmh1FYXiPpvsvw==", + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/signature-v4-multi-region": "^3.996.39", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.9.0" } }, - "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/middleware-sdk-sqs": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.35.tgz", - "integrity": "sha512-J1u7OABwjjBB7sUJiET05dPVAfjFeR6vfM5//DInKuPzc7PhPVNEviC7RqbsIsBdZi8xICVnHemFrTuvGmhySA==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } + "license": "MIT" }, - "node_modules/@aws-sdk/middleware-sdk-sqs/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" + "node_modules/@commander-js/extra-typings": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-10.0.3.tgz", + "integrity": "sha512-OIw28QV/GlP8k0B5CJTRsl8IyNvd0R8C8rfo54Yz9P388vCNDgdNrFlKxZTGqps+5j6lSw3Ss9JTQwcur1w1oA==", + "license": "MIT", + "peerDependencies": { + "commander": "10.0.x" + } }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.31.tgz", - "integrity": "sha512-BDHTpwcsZHEBNEJzOg/B1BkFYJxAXY50dau/NyVWs3d51F0WgIUGSWZot/Os+N3KpDhXeaXnz37mWffAvduREw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/signature-v4-multi-region": "^3.996.39", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/fetch-http-handler": "^5.6.4", - "@smithy/node-http-handler": "^4.9.4", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": ">=20.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@aws-sdk/nested-clients/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.39.tgz", - "integrity": "sha512-8+srXqYIF8KYMLC4FxMLEM5Ek7kUNibJu1R4m8/fUhhNYIZZz26oGtKkCr8I/HiG2fFQxBvaGgQZT4/mqRCSnA==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.0", - "@smithy/signature-v4": "^5.6.3", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, "engines": { - "node": ">=20.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "license": "0BSD" + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1083.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1083.0.tgz", - "integrity": "sha512-s0woKnxuHrExLc5L2ArIH5BMkbonHPtt+5hSBM8oknp9M6QTuUmmAmJ2E0EdzCGONrO+8+ADPqvv6UX0nNcc7A==", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.975.1", - "@aws-sdk/nested-clients": "^3.997.31", - "@aws-sdk/types": "^3.974.0", - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@aws-sdk/token-providers/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/types": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.0.tgz", - "integrity": "sha512-QIBrw90CDm4O0UaIIzkU6DrFdeJzEb2Va5EPEVpyldj6sHJxB6cshhStJuhZxk3wR3PmjJlYsjPmY1kNb+KGBg==", + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "dependencies": { - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "ms": "^2.1.3" }, "engines": { - "node": ">=20.0.0" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@aws-sdk/types/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.34.tgz", - "integrity": "sha512-wHhWL1y7sN3enBA8POrPpQM5jCcmu2ozyhbRei4c8OjVcEaEs6yLucLa/pla457ggS/ysuy7bosagz3HaJkZXA==", + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" + "@eslint/core": "^1.2.1" }, "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", - "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/abort-controller": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", - "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", - "dev": true, - "license": "MIT", "dependencies": { - "tslib": "^2.2.0" + "@types/json-schema": "^7.0.15" }, "engines": { - "node": ">=12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@azure/arm-containerinstance": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@azure/arm-containerinstance/-/arm-containerinstance-9.1.0.tgz", - "integrity": "sha512-N9T3/HJwWXvJuz7tin+nO+DYYCTGHILJ5Die3TtdF8Wd1ITfXGqB0vY/wOnspUu/AGojhaIKGmawAfPdw2kX8w==", + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^1.0.0", - "@azure/core-auth": "^1.3.0", - "@azure/core-client": "^1.7.0", - "@azure/core-lro": "^2.5.0", - "@azure/core-paging": "^1.2.0", - "@azure/core-rest-pipeline": "^1.8.0", - "tslib": "^2.2.0" - }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@azure/core-auth": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", - "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-util": "^1.13.0", - "tslib": "^2.6.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=20.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.6.2" + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" }, "engines": { - "node": ">=18.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@azure/core-auth/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-client": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.2.tgz", - "integrity": "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", - "tslib": "^2.6.2" + "@humanfs/types": "^0.15.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=18.18.0" } }, - "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.6.2" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=18.18.0" } }, - "node_modules/@azure/core-client/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, - "license": "0BSD" + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } }, - "node_modules/@azure/core-http-compat": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", - "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=20.0.0" + "node": ">=12.22" }, - "peerDependencies": { - "@azure/core-client": "^1.10.0", - "@azure/core-rest-pipeline": "^1.22.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@azure/core-http-compat/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=18.0.0" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@azure/core-http-compat/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-lro": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz", - "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-util": "^1.2.0", - "@azure/logger": "^1.0.0", - "tslib": "^2.6.2" - }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=18.0.0" + "node": ">=18" } }, - "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "tslib": "^2.6.2" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, - "node_modules/@azure/core-lro/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-paging": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", - "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, "engines": { - "node": ">=18.0.0" + "node": ">=8" } }, - "node_modules/@azure/core-paging/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-rest-pipeline": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.24.0.tgz", - "integrity": "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==", + "node_modules/@jest/console": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", + "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.10.0", - "@azure/core-tracing": "^1.3.0", - "@azure/core-util": "^1.13.0", - "@azure/logger": "^1.3.0", - "@typespec/ts-http-runtime": "^0.3.4", - "tslib": "^2.6.2" + "@jest/types": "^28.1.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "slash": "^3.0.0" }, "engines": { - "node": ">=20.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/core": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", + "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@jest/console": "^28.1.3", + "@jest/reporters": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^28.1.3", + "jest-config": "^28.1.3", + "jest-haste-map": "^28.1.3", + "jest-message-util": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-resolve": "^28.1.3", + "jest-resolve-dependencies": "^28.1.3", + "jest-runner": "^28.1.3", + "jest-runtime": "^28.1.3", + "jest-snapshot": "^28.1.3", + "jest-util": "^28.1.3", + "jest-validate": "^28.1.3", + "jest-watcher": "^28.1.3", + "micromatch": "^4.0.4", + "pretty-format": "^28.1.3", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-tracing": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", - "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@azure/core-tracing/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-util": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", - "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@typespec/ts-http-runtime": "^0.3.0", - "tslib": "^2.6.2" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "engines": { - "node": ">=20.0.0" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/environment": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", + "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@jest/fake-timers": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/node": "*", + "jest-mock": "^28.1.3" }, "engines": { - "node": ">=18.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/core-util/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/core-xml": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.5.1.tgz", - "integrity": "sha512-xcNRHqCoSp4AunOALEae6A8f3qATb83gSrm31Iqb01OzblvC3/W/bfXozcq78EzIdzZzuH1bZ2NvRR0TdX709w==", + "node_modules/@jest/expect": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", + "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", "dev": true, "license": "MIT", "dependencies": { - "fast-xml-parser": "^5.5.9", - "tslib": "^2.8.1" + "expect": "^28.1.3", + "jest-snapshot": "^28.1.3" }, "engines": { - "node": ">=20.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/core-xml/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/identity": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", - "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "node_modules/@jest/expect-utils": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", + "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.0.0", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.2", - "@azure/core-rest-pipeline": "^1.17.0", - "@azure/core-tracing": "^1.0.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.0.0", - "@azure/msal-browser": "^5.5.0", - "@azure/msal-node": "^5.1.0", - "open": "^10.1.0", - "tslib": "^2.2.0" + "jest-get-type": "^28.0.2" }, "engines": { - "node": ">=20.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/identity/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/fake-timers": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", + "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@jest/types": "^28.1.3", + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "*", + "jest-message-util": "^28.1.3", + "jest-mock": "^28.1.3", + "jest-util": "^28.1.3" }, "engines": { - "node": ">=18.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/identity/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/logger": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", - "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "node_modules/@jest/globals": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", + "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", "dev": true, "license": "MIT", "dependencies": { - "@typespec/ts-http-runtime": "^0.3.0", - "tslib": "^2.6.2" + "@jest/environment": "^28.1.3", + "@jest/expect": "^28.1.3", + "@jest/types": "^28.1.3" }, "engines": { - "node": ">=20.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/logger/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/msal-browser": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.17.0.tgz", - "integrity": "sha512-/yTnW2TCk9Mh+2b/NOaHAN+MryUNxzRTaJD/YtrqOA9bpBWfTXn/iyReRbaLrK/btBo3stEzLyEvuWp2NZ5DuA==", + "node_modules/@jest/reporters": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", + "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", "dev": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "16.11.1" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^28.1.3", + "@jest/test-result": "^28.1.3", + "@jest/transform": "^28.1.3", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^28.1.3", + "jest-util": "^28.1.3", + "jest-worker": "^28.1.3", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-common": { - "version": "16.11.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.11.1.tgz", - "integrity": "sha512-yPohvMwWLv1XnaWnIUyKUh8CvcVChCGqG/VluGwfGmaAfrZTNt5yQ+sIs462Sgw6+e2K83KGmMJ860p73ZSCrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@azure/msal-node": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-5.4.0.tgz", - "integrity": "sha512-6EZEParwHRlnSSIikw8FNAnAzwmh71uhveUXdPNFeZFviJ9SH+rwFiurhjzXqICYTrpm3E+dj693QOwfPbJXAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/msal-common": "16.11.1", - "jsonwebtoken": "^9.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@azure/storage-blob": { - "version": "12.33.0", - "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.33.0.tgz", - "integrity": "sha512-2SX8oP8PyblUcAFZSg39c8Ls+tFjavM6sBeV+qpw33mRzRhI/5hrFJmJ/x0H9xx5l6ECPvgSP8uPxqTeVbHNIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.3", - "@azure/core-http-compat": "^2.2.0", - "@azure/core-lro": "^2.2.0", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.1", - "@azure/core-tracing": "^1.2.0", - "@azure/core-util": "^1.11.0", - "@azure/core-xml": "^1.4.5", - "@azure/logger": "^1.1.4", - "@azure/storage-common": "^12.4.1", - "events": "^3.0.0", - "tslib": "^2.8.1" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "engines": { - "node": ">=22.0.0" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/@azure/storage-blob/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/reporters/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "tslib": "^2.6.2" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=18.0.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@azure/storage-blob/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/storage-common": { - "version": "12.4.1", - "resolved": "https://registry.npmjs.org/@azure/storage-common/-/storage-common-12.4.1.tgz", - "integrity": "sha512-t14unw/WofGDUi7TKJrsyXyPsN+NLgRm7hMaq0llxNmTIzt7f257+6LE6FKIJPh88zLj6M7LPvzve0fEYg/L3A==", + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-http-compat": "^2.2.0", - "@azure/core-rest-pipeline": "^1.24.0", - "@azure/core-tracing": "^1.2.0", - "@azure/core-util": "^1.11.0", - "@azure/logger": "^1.1.4", - "events": "^3.3.0", - "tslib": "^2.8.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=20.0.0" + "node": "*" } }, - "node_modules/@azure/storage-common/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/schemas": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", + "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@sinclair/typebox": "^0.24.1" }, "engines": { - "node": ">=18.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/storage-common/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@azure/storage-queue": { - "version": "12.31.0", - "resolved": "https://registry.npmjs.org/@azure/storage-queue/-/storage-queue-12.31.0.tgz", - "integrity": "sha512-OzhvKO7G8+EXkUQPbKFkO7JNIAMkQZ9gMyZ85dsTCOk8jRRCbGbn3wsk3qrCAf6XWm+E2/fUsdLbX4YCOAWEKw==", + "node_modules/@jest/source-map": { + "version": "28.1.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", + "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", "dev": true, "license": "MIT", "dependencies": { - "@azure/abort-controller": "^2.1.2", - "@azure/core-auth": "^1.9.0", - "@azure/core-client": "^1.9.3", - "@azure/core-http-compat": "^2.0.0", - "@azure/core-paging": "^1.6.2", - "@azure/core-rest-pipeline": "^1.19.1", - "@azure/core-tracing": "^1.2.0", - "@azure/core-util": "^1.11.0", - "@azure/core-xml": "^1.4.3", - "@azure/logger": "^1.1.4", - "@azure/storage-common": "^12.4.1", - "tslib": "^2.8.1" + "@jridgewell/trace-mapping": "^0.3.13", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" }, "engines": { - "node": ">=22.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/storage-queue/node_modules/@azure/abort-controller": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", - "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "node_modules/@jest/test-result": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", + "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", "dev": true, "license": "MIT", "dependencies": { - "tslib": "^2.6.2" + "@jest/console": "^28.1.3", + "@jest/types": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": ">=18.0.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@azure/storage-queue/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "node_modules/@jest/test-sequencer": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", + "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@jest/test-result": "^28.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "slash": "^3.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", - "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", - "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@commander-js/extra-typings": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-10.0.3.tgz", - "integrity": "sha512-OIw28QV/GlP8k0B5CJTRsl8IyNvd0R8C8rfo54Yz9P388vCNDgdNrFlKxZTGqps+5j6lSw3Ss9JTQwcur1w1oA==", - "license": "MIT", - "peerDependencies": { - "commander": "10.0.x" - } - }, - "node_modules/@datadog/datadog-api-client": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@datadog/datadog-api-client/-/datadog-api-client-1.60.0.tgz", - "integrity": "sha512-XmqT/TmkULWkeP2UJJVDWdXS8bdEOBYVlPI6MsSpRKViBwK3t20oPO5kTiTcO6bvipCahh/Yvqjjxeu9fR49HQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/buffer-from": "^1.1.0", - "@types/node": "*", - "@types/pako": "^1.0.3", - "buffer-from": "^1.1.2", - "cross-fetch": "^3.1.5", - "form-data": "^4.0.4", - "loglevel": "^1.8.1", - "pako": "^2.0.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@faker-js/faker": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", - "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", - "npm": ">=10" - } - }, - "node_modules/@grpc/grpc-js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", - "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/proto-loader": "^0.8.0", - "@js-sdsl/ordered-map": "^4.4.2" - }, - "engines": { - "node": ">=12.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", - "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "lodash.camelcase": "^4.3.0", - "long": "^5.0.0", - "protobufjs": "^7.5.5", - "yargs": "^17.7.2" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@inquirer/ansi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", - "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/checkbox": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", - "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.1.21", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", - "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.3.2", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", - "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/editor": { - "version": "4.2.23", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", - "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/external-editor": "^1.0.3", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", - "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", - "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", - "dev": true, - "license": "MIT", - "dependencies": { - "chardet": "^2.1.1", - "iconv-lite": "^0.7.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", - "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", - "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", - "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.23", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", - "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", - "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/search": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", - "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", - "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz", - "integrity": "sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/core": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz", - "integrity": "sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/reporters": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^28.1.3", - "jest-config": "^28.1.3", - "jest-haste-map": "^28.1.3", - "jest-message-util": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-resolve": "^28.1.3", - "jest-resolve-dependencies": "^28.1.3", - "jest-runner": "^28.1.3", - "jest-runtime": "^28.1.3", - "jest-snapshot": "^28.1.3", - "jest-util": "^28.1.3", - "jest-validate": "^28.1.3", - "jest-watcher": "^28.1.3", - "micromatch": "^4.0.4", - "pretty-format": "^28.1.3", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/environment": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz", - "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/node": "*", - "jest-mock": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz", - "integrity": "sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^28.1.3", - "jest-snapshot": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz", - "integrity": "sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^28.0.2" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz", - "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^28.1.3", - "@sinonjs/fake-timers": "^9.1.2", - "@types/node": "*", - "jest-message-util": "^28.1.3", - "jest-mock": "^28.1.3", - "jest-util": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz", - "integrity": "sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^28.1.3", - "@jest/expect": "^28.1.3", - "@jest/types": "^28.1.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz", - "integrity": "sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^28.1.3", - "@jest/test-result": "^28.1.3", - "@jest/transform": "^28.1.3", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^28.1.3", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "28.1.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz", - "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.13", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz", - "integrity": "sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^28.1.3", - "@jest/types": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz", - "integrity": "sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^28.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "slash": "^3.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@js-sdsl/ordered-map": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", - "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/js-sdsl" - } - }, - "node_modules/@jsep-plugin/assignment": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", - "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@jsep-plugin/regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", - "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - }, - "peerDependencies": { - "jsep": "^0.4.0||^1.0.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/file-exists/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", - "license": "MIT" - }, - "node_modules/@nodable/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ], - "license": "MIT" - }, - "node_modules/@oclif/core": { - "version": "4.11.14", - "resolved": "https://registry.npmjs.org/@oclif/core/-/core-4.11.14.tgz", - "integrity": "sha512-cZ5Ktd+rT0PO+o7KBH4vRFTgg+xMLf8F41WK39p8MkXEViZA/Qqe+4lzZT6102zgUxMORET1HtF9t5w8CB3tnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.2", - "ansis": "^3.17.0", - "clean-stack": "^3.0.1", - "cli-spinners": "^2.9.2", - "debug": "^4.4.3", - "ejs": "^3.1.10", - "get-package-type": "^0.1.0", - "indent-string": "^4.0.0", - "is-wsl": "^2.2.0", - "lilconfig": "^3.1.3", - "minimatch": "^10.2.5", - "semver": "^7.8.1", - "string-width": "^4.2.3", - "supports-color": "^8", - "tinyglobby": "^0.2.17", - "widest-line": "^3.1.0", - "wordwrap": "^1.0.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@oclif/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@oclif/core/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/@oclif/plugin-help": { - "version": "6.2.53", - "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-6.2.53.tgz", - "integrity": "sha512-njx2nTH87EQEEuz4ShNtL0gzzN981MRkDPqScbu+Tkd7NpIv30OHdTjQK1GzGtkf+V2RvSYIrX+LrLsUVh9DJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oclif/core": "^4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@oclif/plugin-not-found": { - "version": "3.2.88", - "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-3.2.88.tgz", - "integrity": "sha512-5YTKSpuV77910/iGnGsj0fr9kOazCy/h1brki1CocbfawdvaVPedcwCH25wMcdRfo8mFD656bG9/kdki/+Wb9A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@inquirer/prompts": "^7.10.1", - "@oclif/core": "^4.11.7", - "ansis": "^3.17.0", - "fast-levenshtein": "^3.0.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@oclif/plugin-not-found/node_modules/fast-levenshtein": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-3.0.0.tgz", - "integrity": "sha512-hKKNajm46uNmTlhHSyZkmToAc56uZJwYq7yrciZjqOxnlfQwERDQJmHPUp7m1m9wx8vgOe8IaCKZ5Kv2k1DdCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fastest-levenshtein": "^1.0.7" - } - }, - "node_modules/@opensearch-project/opensearch": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz", - "integrity": "sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==", - "license": "Apache-2.0", - "dependencies": { - "aws4": "^1.11.0", - "debug": "^4.3.1", - "hpagent": "^1.2.0", - "json11": "^2.0.0", - "ms": "^2.1.3", - "secure-json-parse": "^2.4.0" - }, - "engines": { - "node": ">=10", - "yarn": "^1.22.10" - } - }, - "node_modules/@opensearch-project/opensearch/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.218.0.tgz", - "integrity": "sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.9.0.tgz", - "integrity": "sha512-OQ0vzvbZBiUhjqLnUaoNfYmP8553Crr3aggB4y0ZUi815mZ7idpdJXQmoKdeBKJelYttoBlLSSHubmyw3wvX4w==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.1.tgz", - "integrity": "sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-YapQ9vNMX0NSZF6LK5pWAFfjpJleV2O9uYWfYGeb/5F1Kb9rPGK8tZDMJFa/sOksgdFuflDvYuA0B4qjDB4fjQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.218.0.tgz", - "integrity": "sha512-bV7d2OuMpZu2+gAaxUAhzfZ0h3WVZk8ETQUEE3DNSntbTaMpuITjtm8I0rNyHFdm7Ax57K6ty7SgFXlBmOLIvQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.218.0.tgz", - "integrity": "sha512-ubLddKjWULhla9YZRCj/rTBeppjJYE4e9w0icx5mTu3eFhWjQzbV75NYjXuIlEG+NJsBl6d+sTFw5Qu+oej4oQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/exporter-metrics-otlp-http": "0.218.0", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-metrics": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.218.0.tgz", - "integrity": "sha512-3fXxVQEj9TNAFaCi79JeFKfeLd0sDtInaR3gaZDVlzNSPHtz8PZuCV34JKWjD4XXzT20IdMe8IpX6mRVNDA4Tw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.218.0.tgz", - "integrity": "sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.218.0.tgz", - "integrity": "sha512-r1Msf8SNLRmwh9J6XQ5uh82D7CdDWMNHnPB7LAVHjzut0TkSeKc5KcIvr4SvHvfk/xwN5gxC+VLKQ1k0o8PSPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-2.9.0.tgz", - "integrity": "sha512-RwINoce2BH8T4obT5pMcAla2sWma1YZvYuaktWmTluQ0PkQdvv5D060rWI1+kawX+J2qBRcMbwrZJJNcMJUauQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.218.0.tgz", - "integrity": "sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.218.0.tgz", - "integrity": "sha512-H/lCGJ536N98VpYJOaWTQOkv4Dx6TnmStK6Rqfu1W7KkFbPAx04hjdYEMZF/YbnHzPUSIK4kM6OE2GKGBTpV9A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/otlp-exporter-base": "0.218.0", - "@opentelemetry/otlp-transformer": "0.218.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.218.0.tgz", - "integrity": "sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/sdk-logs": "0.218.0", - "@opentelemetry/sdk-metrics": "2.7.1", - "@opentelemetry/sdk-trace-base": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.1.tgz", - "integrity": "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.7.1.tgz", - "integrity": "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", - "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.218.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.218.0.tgz", - "integrity": "sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.218.0", - "@opentelemetry/core": "2.7.1", - "@opentelemetry/resources": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.1.tgz", - "integrity": "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.7.1", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", - "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", - "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", - "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.9.0", - "@opentelemetry/resources": "2.9.0", - "@opentelemetry/sdk-trace": "2.9.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace/node_modules/@opentelemetry/core": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", - "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.42.0.tgz", - "integrity": "sha512-icc5xCzndZfhuJMy5oqk5AvloWquR7jtae74qzpkKkhGp8BivK+oCcEXgGnjCdTfp8hA44l+w8gE8yYJbocJJw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/browser-chromium": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@playwright/browser-chromium/-/browser-chromium-1.61.0.tgz", - "integrity": "sha512-bBGzN+jow5uQtYRDCnFv5cpyMjUYTF6WnsfRRrDXIwbaGDtdh0JdOPmlKdJH4OK7TbkrZ35cV7jhQ1zELoX5ew==", - "dev": true, - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.61.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@playwright/test": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", - "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright": "1.61.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@scarf/scarf": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", - "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", - "hasInstallScript": true, - "license": "Apache-2.0" - }, - "node_modules/@sec-ant/readable-stream": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", - "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", - "license": "MIT", - "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" - } - }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^1.7.0" - } - }, - "node_modules/@smithy/core": { - "version": "3.29.2", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.2.tgz", - "integrity": "sha512-DXUk6yU0C1Q1tYvJh1VCtl8QOBcSoZpKwjTPkxT6A4MUQYHvgeKGByL8mrEdxnvhdf9nq5GyzmRb5n/vPgu3Lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/core/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.7.tgz", - "integrity": "sha512-UEMLOoA0Fl4uYBxh6l0uN0H6EJe/A89OGeDNTteQeXpJ20BcpfIr4wlCY9pel1jEAUHAxaYwuqrYlrKdXE1GKQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.4.tgz", - "integrity": "sha512-psnst7NZWdAEvJvyW8YZEE7xNVMyLrQFfHtyrVFrxNyy+dKWkQ+rqC6oI5ZhxThpUy9RSfEshgm34zqbOxzsRw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/middleware-compression": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.5.7.tgz", - "integrity": "sha512-+6KtzRfzBNLQHQXmTBnUJyUAaq8cYp5GPGJSab3ahsVEgdnC/p/TO3BAyvXw9I5F+hDnwjUAV1EGQCD4NcQOiw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "fflate": "0.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-compression/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.4.tgz", - "integrity": "sha512-BNTop/fSOptmoVk8g+efwHCofFh37g70OWGAFES1TeAAJja1K5aAI8rTE26ETSc5k8IQuWY2kAIoPla01NgYrA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-http-handler/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/signature-v4": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.3.tgz", - "integrity": "sha512-8qVKKzqh7naF27ePmx0SkUfnGP/wBI9dyaeAmhHvopnbIlItUAmB/e6PkPCU3rRb2v9BY8D4EZXSoydSibatvw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.2", - "@smithy/types": "^4.16.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@smithy/types": { - "version": "4.16.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.0.tgz", - "integrity": "sha512-aVUabzlBBmY0PfvVgLKQSOGFIL5/7R54JE3uD9a5Ay/jSED61SkuAcCYENNXJzYUvJ1NPrWO0P+rAXHCkbBUKw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/buffer-from": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@types/buffer-from/-/buffer-from-1.1.3.tgz", - "integrity": "sha512-2lq4YC9uLUMGHkl2IDtX4tCXSo2+hwMpOJcY1qiIk1kybc31rIlPyM1HCVJhkPFIo75a/pOVxqyvwuf5TpCG/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/graceful-fs": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } - }, - "node_modules/@types/pako": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.7.tgz", - "integrity": "sha512-YBtzT2ztNF6R/9+UXj2wTGFnC9NklAnASt3sC0h2m1bbH7G6FyBIkt4AN8ThZpNfxUo1b2iMVO0UawiJymEt8A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/prettier": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", - "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.6.tgz", - "integrity": "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==", - "dev": true, - "license": "MIT", - "dependencies": { - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@typespec/ts-http-runtime/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@typespec/ts-http-runtime/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/@upstash/redis": { - "version": "1.38.0", - "resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.38.0.tgz", - "integrity": "sha512-wu+dZBptlLy0+MCUEoHmzrY/TnmgDey3+c7EbIGwrLqAvkP8yi5MWZHYGIFtAygmL4Bkz2TdFu+eU0vFPncIcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "uncrypto": "^0.1.3" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", - "license": "MIT", - "engines": { - "node": ">=12.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@jest/transform": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", + "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "@babel/core": "^7.11.6", + "@jest/types": "^28.1.3", + "@jridgewell/trace-mapping": "^0.3.13", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "graceful-fs": "^4.2.9", + "jest-haste-map": "^28.1.3", + "jest-regex-util": "^28.0.2", + "jest-util": "^28.1.3", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/@jest/types": { + "version": "28.1.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", + "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", "dev": true, "license": "MIT", "dependencies": { - "type-fest": "^0.21.3" + "@jest/schemas": "^28.1.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansis": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", - "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=6.0.0" } }, - "node_modules/anynum": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", - "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], "license": "MIT" }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { - "sprintf-js": "~1.0.2" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/array-flatten": { + "node_modules/@kwsites/file-exists": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/arrivals": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/arrivals/-/arrivals-2.1.2.tgz", - "integrity": "sha512-g3+rxhxUen2H4+PPBOz6U6pkQ4esBuQPna1rPskgK1jamBdDZeoppyB2vPUM/l0ccunwRrq4r2rKgCvc2FnrFA==", - "dev": true, - "license": "ISC", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "license": "MIT", "dependencies": { - "debug": "^4.0.1", - "nanotimer": "0.3.14" + "debug": "^4.1.1" } }, - "node_modules/arrivals/node_modules/debug": { + "node_modules/@kwsites/file-exists/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4994,95 +1241,34 @@ } } }, - "node_modules/artillery": { - "version": "2.0.33", - "resolved": "https://registry.npmjs.org/artillery/-/artillery-2.0.33.tgz", - "integrity": "sha512-7CiejAL4/stcm9o4GJ8yq1xAKNR39pgVmy2hvb+s9IBCMRzmSYSaRk1Lv/3nWxRtzan0C1LK62lM09MZDKJsfA==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@artilleryio/int-commons": "2.24.0", - "@artilleryio/int-core": "2.28.0", - "@aws-sdk/client-cloudwatch": "^3.1034.0", - "@aws-sdk/client-cloudwatch-logs": "^3.1034.0", - "@aws-sdk/client-ec2": "^3.1034.0", - "@aws-sdk/client-ecs": "^3.1034.0", - "@aws-sdk/client-iam": "^3.1034.0", - "@aws-sdk/client-lambda": "^3.1034.0", - "@aws-sdk/client-s3": "^3.1034.0", - "@aws-sdk/client-sqs": "^3.1034.0", - "@aws-sdk/client-ssm": "^3.1034.0", - "@aws-sdk/client-sts": "^3.1034.0", - "@aws-sdk/credential-providers": "^3.1034.0", - "@azure/arm-containerinstance": "^9.1.0", - "@azure/identity": "^4.13.0", - "@azure/storage-blob": "^12.30.0", - "@azure/storage-queue": "^12.29.0", - "@oclif/core": "^4.8.0", - "@oclif/plugin-help": "^6.2.36", - "@oclif/plugin-not-found": "^3.2.73", - "@smithy/core": "^3.24.0", - "@upstash/redis": "^1.36.1", - "artillery-engine-playwright": "1.30.0", - "artillery-plugin-apdex": "1.24.0", - "artillery-plugin-ensure": "1.27.0", - "artillery-plugin-expect": "2.27.0", - "artillery-plugin-fake-data": "1.24.0", - "artillery-plugin-metrics-by-endpoint": "1.27.0", - "artillery-plugin-publish-metrics": "2.38.0", - "artillery-plugin-slack": "1.22.0", - "async": "^2.6.4", - "chalk": "^2.4.2", - "chokidar": "^3.6.0", - "ci-info": "^4.3.1", - "cli-table3": "^0.6.5", - "cross-spawn": "^7.0.6", - "csv-parse": "^4.16.3", - "debug": "^4.4.3", - "dotenv": "^16.6.1", - "driftless": "^2.0.3", - "esbuild-wasm": "^0.19.12", - "eventemitter3": "^5.0.4", - "fs-extra": "^11.3.3", - "got": "^14.6.6", - "joi": "^17.13.3", - "js-yaml": "^3.14.1", - "jsonwebtoken": "^9.0.3", - "lodash": "^4.18.0", - "moment": "^2.30.1", - "nanoid": "^3.3.4", - "ora": "^4.0.4", - "rc": "^1.2.8", - "sqs-consumer": "6.0.2", - "tempy": "3.1.0", - "walk-sync": "^0.2.3", - "yaml-js": "^0.3.1" - }, - "bin": { - "artillery": "bin/run" - }, - "engines": { - "node": ">= 22.13.0" - } + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "license": "MIT" }, - "node_modules/artillery-engine-playwright": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/artillery-engine-playwright/-/artillery-engine-playwright-1.30.0.tgz", - "integrity": "sha512-Anc9SJUL/969XTUjEGzYiTZMCg7C39VNpVNLCDgkvZ8DmGdDqcqgXl6yTymwzyJpJE7leo3x5t/r5ADUYLPdDw==", - "dev": true, - "license": "MPL-2.0", + "node_modules/@opensearch-project/opensearch": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz", + "integrity": "sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==", + "license": "Apache-2.0", "dependencies": { - "@playwright/browser-chromium": "1.61.0", - "@playwright/test": "1.61.0", - "debug": "^4.4.3", - "playwright": "1.61.0" + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "json11": "^2.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" } }, - "node_modules/artillery-engine-playwright/node_modules/debug": { + "node_modules/@opensearch-project/opensearch/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -5096,413 +1282,333 @@ } } }, - "node_modules/artillery-plugin-apdex": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-apdex/-/artillery-plugin-apdex-1.24.0.tgz", - "integrity": "sha512-66sHtp8mtvSBkWfDaehMkERAEMc1NaOI6/i5hJikFP5iSZk3UC3PWBEeeyl1NGd39J1YpuFEA1aEn8ua048BYw==", + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@simple-git/args-pathspec": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", + "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", + "license": "MIT" + }, + "node_modules/@simple-git/argv-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", + "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "license": "MIT", + "dependencies": { + "@simple-git/args-pathspec": "^1.0.3" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", "dev": true, - "license": "MPL-2.0" + "license": "MIT" }, - "node_modules/artillery-plugin-ensure": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-ensure/-/artillery-plugin-ensure-1.27.0.tgz", - "integrity": "sha512-2ePl9SY/O8rrViqY+KLwMjCn5zVmgmAUo+30ATuisBsJcxZ+Z2g+NaMVZ0AVB8CZNGrqaIxTShbH42i/EPgVKA==", + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, - "license": "MPL-2.0", + "license": "BSD-3-Clause", "dependencies": { - "chalk": "^2.4.2", - "debug": "^4.4.3", - "filtrex": "^2.2.3" + "type-detect": "4.0.8" } }, - "node_modules/artillery-plugin-ensure/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" + "@sinonjs/commons": "^1.7.0" } }, - "node_modules/artillery-plugin-ensure/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/artillery-plugin-ensure/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "1.1.3" + "@babel/types": "^7.0.0" } }, - "node_modules/artillery-plugin-ensure/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/artillery-plugin-ensure/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/artillery-plugin-ensure/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.8.0" + "dependencies": { + "@babel/types": "^7.28.2" } }, - "node_modules/artillery-plugin-ensure/node_modules/filtrex": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/filtrex/-/filtrex-2.2.3.tgz", - "integrity": "sha512-TL12R6SckvJdZLibXqyp4D//wXZNyCalVYGqaWwQk9zucq9dRxmrJV4oyuRq4PHFHCeV5ZdzncIc/Ybqv1Lr6Q==", + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, "license": "MIT" }, - "node_modules/artillery-plugin-ensure/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/artillery-plugin-ensure/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" + "@types/node": "*" } }, - "node_modules/artillery-plugin-expect": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-expect/-/artillery-plugin-expect-2.27.0.tgz", - "integrity": "sha512-ZdXoNRb5xvZCdZFQMzJsf8VY508WMUQ0ou3dkIqmQumAUD68lLNZMQM9j9yjaniG1ivSy4OCxWle52J5Et3WSA==", + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true, - "license": "SEE LICENSE IN LICENSE.txt", - "dependencies": { - "chalk": "^4.1.2", - "debug": "^4.4.3", - "jmespath": "^0.16.0", - "lodash": "^4.18.0" - } + "license": "MIT" }, - "node_modules/artillery-plugin-expect/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@types/istanbul-lib-coverage": "*" } }, - "node_modules/artillery-plugin-fake-data": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-fake-data/-/artillery-plugin-fake-data-1.24.0.tgz", - "integrity": "sha512-Ft7GWaFCxRKUzlR1wBpNo6mJgmTHTcN4F7HAye5vx+svJagsyD5UmlBHLYFatJ8DiFR75sJepQE/AMrhTpziHg==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, - "license": "MPL-2.0", + "license": "MIT", "dependencies": { - "@faker-js/faker": "10.4.0" + "@types/istanbul-lib-report": "*" } }, - "node_modules/artillery-plugin-metrics-by-endpoint": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-metrics-by-endpoint/-/artillery-plugin-metrics-by-endpoint-1.27.0.tgz", - "integrity": "sha512-XQwWgDHyC59dxcONDTjVu9Q8s1HfVnaMGOQb19ll9Hovag3zH0k7M9LYxY9ViPkeJeckGbWoS1BsU3kHifRrzw==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "debug": "^4.4.3" - } + "license": "MIT" }, - "node_modules/artillery-plugin-metrics-by-endpoint/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "undici-types": "~7.18.0" } }, - "node_modules/artillery-plugin-publish-metrics": { - "version": "2.38.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-publish-metrics/-/artillery-plugin-publish-metrics-2.38.0.tgz", - "integrity": "sha512-XatDrJU16dJpdOnCQQ/j446BRIVmepCh3bXvuiwWa7FsJ51eMippSFjBLDjZGPzTjVh/YKovdgd0hWWXVvVmnw==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.1034.0", - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^2.5.1", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.218.0", - "@opentelemetry/exporter-metrics-otlp-http": "^0.218.0", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.218.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.218.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.218.0", - "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0", - "@opentelemetry/exporter-zipkin": "^2.5.1", - "@opentelemetry/resources": "^2.5.1", - "@opentelemetry/sdk-metrics": "^2.5.1", - "@opentelemetry/sdk-trace-base": "^2.5.1", - "@opentelemetry/semantic-conventions": "^1.39.0", - "async": "^2.6.4", - "datadog-metrics": "^0.12.1", - "debug": "^4.4.3", - "dogapi": "^2.8.4", - "got": "^14.6.6", - "hot-shots": "^10.2.1", - "mixpanel": "^0.18.0", - "opentracing": "^0.14.7", - "prom-client": "^14.2.0", - "semver": "^7.7.3", - "uuid": "^11.1.0" - } - }, - "node_modules/artillery-plugin-publish-metrics/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/artillery-plugin-slack": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/artillery-plugin-slack/-/artillery-plugin-slack-1.22.0.tgz", - "integrity": "sha512-Z7quNzgDeLEmOViDdM6tYmlMbXXlrDznG2hoJyXI6sO+/TFF8Ojsld/u0jEWNPmg01EIsqFncuT+ApfcCXG/eA==", + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true, - "license": "MPL-2.0", - "dependencies": { - "debug": "^4.4.3", - "got": "^14.6.6" - } + "license": "MIT" }, - "node_modules/artillery-plugin-slack/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "@types/yargs-parser": "*" } }, - "node_modules/artillery/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "color-convert": "^1.9.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/artillery/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "bin": { + "acorn": "bin/acorn" }, "engines": { - "node": ">=4" + "node": ">=0.4.0" } }, - "node_modules/artillery/node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", - "engines": { - "node": ">=8" + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/artillery/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", "license": "MIT", - "dependencies": { - "color-name": "1.1.3" + "engines": { + "node": ">=12.0" } }, - "node_modules/artillery/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/artillery/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/artillery/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { - "url": "https://dotenvx.com" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/artillery/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">=8" } }, - "node_modules/artillery/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/artillery/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "has-flag": "^3.0.0" + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" }, "engines": { - "node": ">=4" + "node": ">= 8" } }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, "license": "MIT", "dependencies": { - "lodash": "^4.17.14" + "sprintf-js": "~1.0.2" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5678,16 +1784,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5698,27 +1794,9 @@ "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==", - "dev": true, - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -5778,20 +1856,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "dev": true, - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", @@ -5861,13 +1925,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5875,35 +1932,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/byte-counter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", - "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -5913,88 +1941,6 @@ "node": ">= 0.8" } }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "13.0.19", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.19.tgz", - "integrity": "sha512-SVXGH037+Mo1aIMO5B2UcleR43FGjFdN+M8JObSyEoQ2Mn4CODRWx28gN5jiTF0n5ItsgtIZfyargMNs8GX4kg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.2.0", - "get-stream": "^9.0.1", - "http-cache-semantics": "^4.2.0", - "keyv": "^5.6.0", - "mimic-response": "^4.0.0", - "normalize-url": "^8.1.1", - "responselike": "^4.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", - "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sec-ant/readable-stream": "^0.4.1", - "is-stream": "^4.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/is-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", - "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/cacheable-request/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6092,57 +2038,6 @@ "node": ">=10" } }, - "node_modules/chardet": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", - "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cheerio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", - "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.1.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^7.19.0", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=20.18.1" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -6191,87 +2086,6 @@ "dev": true, "license": "MIT" }, - "node_modules/clean-stack": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-3.0.1.tgz", - "integrity": "sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/clean-stack/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -6287,16 +2101,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6405,27 +2209,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-parser/node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -6449,16 +2232,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cross-fetch": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", - "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "node-fetch": "^2.7.0" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6473,72 +2246,6 @@ "node": ">= 8" } }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/csv-parse": { - "version": "4.16.3", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.16.3.tgz", - "integrity": "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg==", - "dev": true, - "license": "MIT" - }, "node_modules/cve-core": { "version": "2.2.0-rc4", "resolved": "git+ssh://git@github.com/CVEProject/cve-core.git#a1640b2979aebb681f151aa20248f87eb4fd8a81", @@ -6578,48 +2285,16 @@ "node": ">= 16.20.2 < 22" } }, - "node_modules/cve-core/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/datadog-metrics": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/datadog-metrics/-/datadog-metrics-0.12.1.tgz", - "integrity": "sha512-Gy+17ia7m9Uy+nKQHDd7fljdq0fqqfpgkpxlwW0x1oFKI7RcgDV32pMCfHtv4HKychP6fHtncj3Lf4VN/g4G6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@datadog/datadog-api-client": "^1.17.0", - "debug": "^4.1.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/datadog-metrics/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "node_modules/cve-core/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", "engines": { - "node": ">=6.0" + "node": ">=12" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/date-fns": { @@ -6656,22 +2331,6 @@ "ms": "^2.1.1" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -6679,26 +2338,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deep-for-each": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/deep-for-each/-/deep-for-each-3.0.0.tgz", - "integrity": "sha512-pPN+0f8jlnNP+z90qqOdxGghJU5XM6oBDhvAR+qdQzjCg5pk/7VPPvKK1GqoXEFkHza6ZS+Otzzvmr0g3VUaKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.isplainobject": "^4.0.6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6716,62 +2355,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -6820,292 +2403,69 @@ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" } }, - "node_modules/dogapi": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/dogapi/-/dogapi-2.8.4.tgz", - "integrity": "sha512-065fsvu5dB0o4+ENtLjZILvXMClDNH/yA9H6L8nsdcNiz9l0Hzpn7aQaCOPYXxqyzq4CRPOdwkFXUjDOXfRGbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend": "^3.0.2", - "json-bigint": "^1.0.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rc": "^1.2.8" - }, - "bin": { - "dogapi": "bin/dogapi" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dotenv": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", - "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=6" - } - }, - "node_modules/driftless": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/driftless/-/driftless-2.0.3.tgz", - "integrity": "sha512-hSDKsQphnL4O0XLAiyWQ8EiM9suXH0Qd4gMtwF86b5wygGV8r95w0JcA38FOmx9N3LjFCIHLG2winLPNken4Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "present": "^0.0.3" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.381", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz", - "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding-sniffer": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", - "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", - "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.21.0", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } + "node_modules/dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=6" } }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.4" } }, - "node_modules/ensure-posix-path": { + "node_modules/ee-first": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", - "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.381", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.381.tgz", + "integrity": "sha512-n9Wa6yB+vDsGuA8AKbl/0z7HbvWqt5jxIdvr1IUicd0ryPrk7/xzwqLv8D9AbbvZ6avVNtXYLTfmgFHkwkyelg==", "dev": true, "license": "ISC" }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=0.12" + "node": ">=12" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, "node_modules/error-ex": { @@ -7163,19 +2523,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild-wasm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.12.tgz", - "integrity": "sha512-Zmc4hk6FibJZBcTx5/8K/4jT3/oG1vkGTEeKJUQFCUQKimD6Q7+adp/bdVQyYJFolMKaXkQnVZdV4O5ZaTYmyQ==", - "dev": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7486,23 +2833,6 @@ "node": ">= 0.6" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -7654,13 +2984,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7682,57 +3005,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-xml-builder": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.1.tgz", - "integrity": "sha512-tPb5TTWfgfVx5BNSi2xV0eLr89POeXXn0dXIsCJ9m1narrWxeIyx6je9d7Rce/3NyXLbvuQmLkxq+RuxMWejvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.9.3.tgz", - "integrity": "sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "@nodable/entities": "^2.2.0", - "fast-xml-builder": "^1.2.0", - "is-unsafe": "^1.0.1", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.4.1", - "xml-naming": "^0.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", - "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -7743,37 +3015,12 @@ "bser": "2.1.1" } }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fetch-retry": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", "license": "MIT" }, - "node_modules/fflate": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", - "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", - "dev": true, - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -7787,37 +3034,6 @@ "node": ">=16.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==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -7831,13 +3047,6 @@ "node": ">=8" } }, - "node_modules/filtrex": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/filtrex/-/filtrex-0.5.4.tgz", - "integrity": "sha512-2phGAjWOYRf96Al6s+w/hMjObP1cRyQ95hoZApjeFO75DXN4Flh9uuUAtL3LI4fkryLa2QWdA8MArvt0GMU0pA==", - "dev": true, - "license": "MIT" - }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -7958,16 +3167,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", - "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -7986,21 +3185,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-extra": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.6.tgz", - "integrity": "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8174,85 +3358,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "14.6.6", - "resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz", - "integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^7.0.1", - "byte-counter": "^0.1.0", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^13.0.12", - "decompress-response": "^10.0.0", - "form-data-encoder": "^4.0.2", - "http2-wrapper": "^2.2.1", - "keyv": "^5.5.3", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^4.0.1", - "responselike": "^4.0.2", - "type-fest": "^4.26.1" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/decompress-response": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", - "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^4.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/got/node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/got/node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/got/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -8309,19 +3414,6 @@ "node": ">= 0.4" } }, - "node_modules/hot-shots": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/hot-shots/-/hot-shots-10.2.1.tgz", - "integrity": "sha512-tmjcyZkG/qADhcdC7UjAp8D7v7W2DOYFgaZ48fYMuayMQmVVUg8fntKmrjes/b40ef6yZ+qt1lB8kuEDfLC4zw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "unix-dgram": "2.x" - } - }, "node_modules/hpagent": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", @@ -8338,46 +3430,6 @@ "dev": true, "license": "MIT" }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/http-errors": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", @@ -8412,52 +3464,6 @@ "node": ">= 0.6" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8507,17 +3513,7 @@ "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.0.0" + "node": ">=10.17.0" } }, "node_modules/iconv-lite": { @@ -8579,16 +3575,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -8607,13 +3593,6 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC" - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8659,22 +3638,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8718,51 +3681,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -8798,32 +3716,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unsafe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-1.0.1.tgz", - "integrity": "sha512-CLK2+VdgERgD96EYm5lUQssZYlRg2tkZnbsxZoacmSiRxiFJ4Nk4SzjCl+Ur+v3kXIY9dTIdb3IH22y1mZ56LA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8944,31 +3836,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jake/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, "node_modules/jest": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz", @@ -9622,30 +4489,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/joi": { - "version": "17.13.4", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", - "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9667,16 +4510,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", - "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.16.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -9690,16 +4523,6 @@ "node": ">=6" } }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9758,84 +4581,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsonpath-plus": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.4.0.tgz", - "integrity": "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsep-plugin/assignment": "^1.3.0", - "@jsep-plugin/regex": "^1.0.4", - "jsep": "^1.4.0" - }, - "bin": { - "jsonpath": "bin/jsonpath-cli.js", - "jsonpath-plus": "bin/jsonpath-cli.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "dev": true, - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dev": true, - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9880,19 +4625,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9919,33 +4651,12 @@ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -9959,189 +4670,36 @@ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT" }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.map": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", - "license": "MIT" - }, - "node_modules/lodash.omit": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", - "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", - "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.reduce": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", - "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", - "license": "MIT" - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", - "license": "MIT" - }, - "node_modules/lodash.unset": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.18.0.tgz", - "integrity": "sha512-7B95k5zjcJtuGuLqxOjT91g0Txwv1PWBlBDGzjW4kM8qgw2qfSgBo1QcIWGUpW9uN/1ZSKX9NHJzVJIgbeR5PQ==", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/log-symbols/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/log-symbols/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-symbols/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/log-symbols/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } + "license": "MIT" }, - "node_modules/log-symbols/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==", + "deprecated": "This package is deprecated. Use destructuring assignment syntax instead.", + "license": "MIT" }, - "node_modules/loglevel": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", - "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "license": "MIT" }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "dev": true, - "license": "Apache-2.0" + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "license": "MIT" }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/lodash.unset": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/lodash.unset/-/lodash.unset-4.18.0.tgz", + "integrity": "sha512-7B95k5zjcJtuGuLqxOjT91g0Txwv1PWBlBDGzjW4kM8qgw2qfSgBo1QcIWGUpW9uN/1ZSKX9NHJzVJIgbeR5PQ==", + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -10179,29 +4737,6 @@ "tmpl": "1.0.5" } }, - "node_modules/matcher-collection": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-1.1.2.tgz", - "integrity": "sha512-YQ/teqaOIIfUHedRam08PB3NK7Mjct6BvzRnJmpGDm8uFXpNr1sbY4yuflI5JcEs6COpYA0FpRQhSDBf1tT95g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minimatch": "^3.0.2" - } - }, - "node_modules/matcher-collection/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -10302,19 +4837,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -10330,16 +4852,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -10349,74 +4861,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mixpanel": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.18.1.tgz", - "integrity": "sha512-YD1xfn6WP6ZLQ6Pmgh0KgdXhueJEsrodThMTsHzHMH0VbWa9ck8s+ynDtM83OSgt+yQ61W/SQNrH8Y4wIwocGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "https-proxy-agent": "5.0.0" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/mixpanel/node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/mixpanel/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mixpanel/node_modules/https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/morgan": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", @@ -10466,50 +4910,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/nan": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", - "integrity": "sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nanotimer": { - "version": "0.3.14", - "resolved": "https://registry.npmjs.org/nanotimer/-/nanotimer-0.3.14.tgz", - "integrity": "sha512-NpKXdP6ZLwZcODvDeyfoDBVoncbrgvC12txO3F4l9BxMycQjZD29AnasGAy7uSi3dcsTGnGn6/zzvQRwbjS4uw==", - "dev": true, - "license": "ISC" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10526,27 +4926,6 @@ "node": ">= 0.6" } }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10644,19 +5023,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-url": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", - "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -10670,19 +5036,6 @@ "node": ">=8" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10749,107 +5102,24 @@ }, "funding": { "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opentracing": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/opentracing/-/opentracing-0.14.7.tgz", - "integrity": "sha512-vz9iS7MJ5+Bp1URw8Khvdyw1H/hGvzHWlKQ7eRrQojSCDL1/SrWfrY9QebLw97n2deyRtzHRC3MkQfVNUCo91Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/ora": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", - "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^3.0.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.2.0", - "is-interactive": "^1.0.0", - "log-symbols": "^3.0.0", - "mute-stream": "0.0.8", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ora/node_modules/mute-stream": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", - "dev": true, - "license": "ISC" + } }, - "node_modules/p-cancelable": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", - "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, "engines": { - "node": ">=14.16" + "node": ">= 0.8.0" } }, "node_modules/p-limit": { @@ -10913,23 +5183,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/pako": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.2.0.tgz", - "integrity": "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "(MIT AND Zlib)" - }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -10949,59 +5202,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "dev": true, - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -11021,22 +5221,6 @@ "node": ">=8" } }, - "node_modules/path-expression-matcher": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", - "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -11137,53 +5321,6 @@ "node": ">=8" } }, - "node_modules/playwright": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", - "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "playwright-core": "1.61.0" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.61.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", - "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11194,13 +5331,6 @@ "node": ">= 0.8.0" } }, - "node_modules/present": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/present/-/present-0.0.3.tgz", - "integrity": "sha512-d0QMXYTKHuAO0n0IfI/x2lbNwybdNWjRQ08hQySzqMQ2M0gwh/IetTv2glkPJihFn+cMDYjK/BiVgcLcjsASgg==", - "dev": true, - "license": "MIT" - }, "node_modules/prettier": { "version": "3.9.3", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.3.tgz", @@ -11246,19 +5376,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/prom-client": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.2.0.tgz", - "integrity": "sha512-sF308EhTenb/pDRPakm+WgiN+VdM/T1RaHj1x+MvAuT8UiQP8JmOEbxVqtkbfR4LrvOg5n7ic01kRBDGXjYikA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tdigest": "^0.1.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -11273,30 +5390,6 @@ "node": ">= 6" } }, - "node_modules/protobufjs": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", - "dev": true, - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.1", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.3.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11351,19 +5444,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -11408,32 +5488,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -11485,13 +5539,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-cwd": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", @@ -11525,43 +5572,6 @@ "node": ">=10" } }, - "node_modules/responselike": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", - "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/restore-cursor/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==", - "dev": true, - "license": "ISC" - }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -11614,19 +5624,6 @@ "node": "*" } }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -11920,79 +5917,6 @@ "node": ">=8" } }, - "node_modules/socket.io-client": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", - "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socketio-wildcard": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/socketio-wildcard/-/socketio-wildcard-2.0.0.tgz", - "integrity": "sha512-Bf3ioZq15Z2yhFLDasRvbYitg82rwm+5AuER5kQvEQHhNFf4R4K5o/h57nEpN7A59T9FyRtTj34HZfMWAruw/A==", - "dev": true, - "license": "MIT" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -12019,39 +5943,7 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/sqs-consumer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/sqs-consumer/-/sqs-consumer-6.0.2.tgz", - "integrity": "sha512-Y8ztFBc1VPj4q72j9Uji4XR6n2O88n9X83aOUaCvz9zZqjpGJaI4PBHBPnE8RDBgD0EQR8Dh+sQwH/+8mzr+Bg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sqs": "^3.226.0", - "debug": "^4.3.4" - }, - "peerDependencies": { - "@aws-sdk/client-sqs": "^3.226.0" - } - }, - "node_modules/sqs-consumer/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", @@ -12150,22 +6042,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz", - "integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "anynum": "^1.0.1" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12278,71 +6154,6 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, - "node_modules/tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bintrees": "1.0.2" - } - }, - "node_modules/temp-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", - "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/tempy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", - "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-stream": "^3.0.0", - "temp-dir": "^3.0.0", - "type-fest": "^2.12.2", - "unique-string": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/tempy/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -12410,43 +6221,6 @@ "node": "*" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -12486,26 +6260,6 @@ "nodetouch": "bin/nodetouch.js" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, "node_modules/ts-pattern": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", @@ -12567,13 +6321,6 @@ "node": ">= 0.6" } }, - "node_modules/uncrypto": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", - "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", - "dev": true, - "license": "MIT" - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -12581,16 +6328,6 @@ "dev": true, "license": "MIT" }, - "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -12598,48 +6335,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unix-dgram": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/unix-dgram/-/unix-dgram-2.0.7.tgz", - "integrity": "sha512-pWaQorcdxEUBFIKjCqqIlQaOoNVmchyoaNAJ/1LwyyfK2XSxcBhgJNiSE8ZRhR0xkNGyk4xInt1G03QPoKXY5A==", - "dev": true, - "hasInstallScript": true, - "license": "ISC", - "optional": true, - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.20.0" - }, - "engines": { - "node": ">=0.10.48" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -12699,20 +6394,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -12753,17 +6434,6 @@ "node": ">= 0.8" } }, - "node_modules/walk-sync": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-0.2.7.tgz", - "integrity": "sha512-OH8GdRMowEFr0XSHQeX5fGweO6zSVHo7bG/0yJQx6LAj9Oukz0C8heI3/FYectT66gY0IPGe89kOvU410/UNpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ensure-posix-path": "^1.0.0", - "matcher-collection": "^1.0.0" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -12774,71 +6444,6 @@ "makeerror": "1.0.12" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "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==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "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==", - "dev": true, - "license": "MIT", - "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", @@ -12854,19 +6459,6 @@ "node": ">= 8" } }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12877,13 +6469,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -12930,85 +6515,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ws": { - "version": "7.5.11", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz", - "integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wsl-utils/node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13026,16 +6532,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml-js": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/yaml-js/-/yaml-js-0.3.1.tgz", - "integrity": "sha512-LjoIFHCtSfkGzPsmYmfDsW+TShtQBcY7lwH1yLQ5brJHXRhjteUnVE2ThCbz1madq8JUZIAjFiavfnIFeTO8CQ==", - "dev": true, - "license": "WTFPL", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -13077,19 +6573,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", - "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index 88265fc..5b656fe 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,6 @@ "version": "0.3.0-beta", "scripts": { "dev": "node routes/swagger.js && nodemon server.js", - "artillery:contract": "artillery run test/artillery/contract.yml", - "artillery:load": "artillery run test/artillery/load.yml", - "artillery:smoke": "artillery run test/artillery/smoke.yml", - "artillery:stress": "artillery run test/artillery/stress.yml", "coverage": "jest --coverage", "format": "prettier --write .", "format:check": "prettier --check .", @@ -30,7 +26,6 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "artillery": "^2.0.33", "eslint": "^10.6.0", "globals": "^17.7.0", "jest": "^28.1.3", diff --git a/test/artillery/.env.example b/test/artillery/.env.example deleted file mode 100644 index aab1d30..0000000 --- a/test/artillery/.env.example +++ /dev/null @@ -1,6 +0,0 @@ -# Copy to test/artillery/.env for local use. Do not commit real URLs or keys. -CVE_SEARCH_API_DEV_URL= -CVE_SEARCH_API_TEST_URL= -CVE_SEARCH_API_INT_URL= -CVE_SEARCH_API_PROD_URL= -ARTILLERY_CLOUD_API_KEY= diff --git a/test/artillery/README.md b/test/artillery/README.md deleted file mode 100644 index 9ae00ce..0000000 --- a/test/artillery/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Artillery API Checks - -These Artillery profiles exercise the live API surface. They require the API to be running and connected to an OpenSearch instance. - -Default target: `http://127.0.0.1:4000` - -Named environments: - -- `local`: `http://127.0.0.1:4000` -- `dev`: reads `CVE_SEARCH_API_DEV_URL` -- `test`: reads `CVE_SEARCH_API_TEST_URL` -- `int`: reads `CVE_SEARCH_API_INT_URL` -- `prod`: reads `CVE_SEARCH_API_PROD_URL` - -Override the target with Artillery's `--target` flag: - -```sh -npm run artillery:smoke -- --target http://127.0.0.1:4100 -npm run artillery:contract -- --target http://127.0.0.1:4100 -npm run artillery:load -- --target http://127.0.0.1:4100 -npm run artillery:stress -- --target http://127.0.0.1:4100 -``` - -Run against a named deployed environment: - -```sh -CVE_SEARCH_API_DEV_URL="$DEV_API_URL" npm run artillery:smoke -- --environment dev -``` - -You can also keep local target values in an untracked env file: - -```sh -cp test/artillery/.env.example test/artillery/.env -npm run artillery:smoke -- --dotenv test/artillery/.env --environment dev -``` - -Run public-search load or stress checks only after the target environment owner approves the traffic volume: - -```sh -CVE_SEARCH_API_TEST_URL="$TEST_API_URL" npm run artillery:load -- --environment test -CVE_SEARCH_API_INT_URL="$INT_API_URL" npm run artillery:stress -- --environment int -``` - -Record a run to Artillery Cloud by setting `ARTILLERY_CLOUD_API_KEY` outside the repository and passing `--record`: - -```sh -ARTILLERY_CLOUD_API_KEY=... \ -CVE_SEARCH_API_DEV_URL="$DEV_API_URL" \ -npm run artillery:smoke -- \ - --environment dev \ - --record \ - --name cve-search-api-smoke-dev \ - --tags service:cve-search-api,env:dev,profile:smoke -``` - -Use matching tags for load and stress runs so Artillery Cloud comparisons stay meaningful: - -```sh -ARTILLERY_CLOUD_API_KEY=... \ -CVE_SEARCH_API_INT_URL="$INT_API_URL" \ -npm run artillery:stress -- \ - --environment int \ - --record \ - --name cve-search-api-stress-int \ - --tags service:cve-search-api,env:int,profile:stress -``` - -If the command fails with `SELF_SIGNED_CERT_IN_CHAIN`, Node does not trust the certificate chain for Artillery Cloud or the HTTPS API target. Use the same host CA bundle used by local Docker builds. `Dockerfile.local` receives that file from `NPM_CA_CERT` and mounts it inside the build as `/run/secrets/npm_ca`; Artillery needs the host path through `NODE_EXTRA_CA_CERTS`: - -```sh -export NODE_EXTRA_CA_CERTS="$NPM_CA_CERT" -``` - -Then rerun the Artillery command. Keep Artillery Cloud keys, deployment URLs, and certificate file paths in local environment variables or an untracked env file. - -Profiles: - -- `smoke.yml`: one virtual user runs the expected successful paths for health, OpenAPI docs, `POST /search`, and `POST /webSearch`. -- `contract.yml`: one virtual user runs negative request-shape checks for validation and removed endpoint behavior. -- `load.yml`: a conservative weighted traffic mix for short local performance checks. -- `stress.yml`: a public-search weighted traffic mix with ramping arrival rates for approved capacity tests. - -Each profile records endpoint-level metrics under stable request names and enforces basic SLO gates: - -- `smoke.yml`: p95 response time under 5000 ms and zero virtual user errors. -- `contract.yml`: p95 response time under 5000 ms and zero virtual user errors. -- `load.yml`: p95 response time under 3000 ms, p99 response time under 8000 ms, and virtual user error rate at or below 1%. -- `stress.yml`: p95 response time under 8000 ms, p99 response time under 15000 ms, and virtual user error rate at or below 5%. - -Use `load.yml` or `stress.yml` against shared or production-like environments only when the target environment owner has approved the traffic volume. The stress profile includes the expensive `POST /search virtualMatchString range` path at low weight so capacity reports show whether public traffic amplifies that query shape. - -When diagnosing slow `/search` requests during a controlled run, set `SearchLogPerformanceTimings=true` on the API deployment before starting the profile. Leave it disabled for normal operation unless the additional structured logs are expected and acceptable. - -These profiles are intentionally separate from `npm run quality` because they depend on live infrastructure and real OpenSearch data. - -See the root README's `Interpreting Artillery Results` section for how to read expectation failures, SLO failures, latency percentiles, virtual user counters, endpoint-level metrics, and Cloud run tags. diff --git a/test/artillery/contract.yml b/test/artillery/contract.yml deleted file mode 100644 index 65d39fd..0000000 --- a/test/artillery/contract.yml +++ /dev/null @@ -1,138 +0,0 @@ -config: - target: 'http://127.0.0.1:4000' - environments: - local: - target: 'http://127.0.0.1:4000' - dev: - target: '{{ $env.CVE_SEARCH_API_DEV_URL }}' - test: - target: '{{ $env.CVE_SEARCH_API_TEST_URL }}' - int: - target: '{{ $env.CVE_SEARCH_API_INT_URL }}' - prod: - target: '{{ $env.CVE_SEARCH_API_PROD_URL }}' - phases: - - duration: 1 - arrivalCount: 1 - name: 'single-user contract' - plugins: - metrics-by-endpoint: - stripQueryString: true - useOnlyRequestNames: true - metricsNamespace: 'cve-search-api.endpoint' - expect: - reportFailuresAsErrors: true - outputFormat: 'prettyError' - ensure: - p95: 5000 - maxErrorRate: 0 - http: - defaults: - headers: - Accept: 'application/json' - Content-Type: 'application/json' - -scenarios: - - name: 'API validation contract' - flow: - - get: - name: 'GET /search removed endpoint' - url: '/search?keywordSearch=apache' - expect: - - statusCode: 404 - - - post: - name: 'POST /search rejects query string' - url: '/search?keywordSearch=apache' - json: {} - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /search rejects invalid date' - url: '/search' - json: - pubStartDate: '2024-02-30T00:00:00.000Z' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /search rejects invalid cveIds' - url: '/search' - json: - cveIds: 'not-a-cve' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /search rejects deprecated cveId' - url: '/search' - json: - cveId: 'CVE-2024-0001' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /search rejects invalid cpeName' - url: '/search' - json: - cpeName: 'cpe:2.3:a:freetype:freetype:*:*:*:*:*:*:*:*' - expect: - - statusCode: 404 - - contentType: 'json' - - - post: - name: 'POST /search rejects range without virtualMatchString' - url: '/search' - json: - versionStart: '2.2' - versionStartType: 'including' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /search rejects virtualMatchString version component' - url: '/search' - json: - virtualMatchString: 'cpe:2.3:o:linux:linux_kernel:*' - versionStart: '2.2' - versionStartType: 'including' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /webSearch rejects blank query' - url: '/webSearch' - json: - query: '' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /webSearch rejects unsupported sort' - url: '/webSearch' - json: - query: 'apache' - sort: - property: 'published' - order: 'desc' - expect: - - statusCode: 400 - - contentType: 'json' - - - post: - name: 'POST /webSearch rejects unsupported field' - url: '/webSearch' - json: - query: 'apache' - unsupported: true - expect: - - statusCode: 400 - - contentType: 'json' diff --git a/test/artillery/load.yml b/test/artillery/load.yml deleted file mode 100644 index b5e127e..0000000 --- a/test/artillery/load.yml +++ /dev/null @@ -1,141 +0,0 @@ -config: - target: 'http://127.0.0.1:4000' - environments: - local: - target: 'http://127.0.0.1:4000' - dev: - target: '{{ $env.CVE_SEARCH_API_DEV_URL }}' - test: - target: '{{ $env.CVE_SEARCH_API_TEST_URL }}' - int: - target: '{{ $env.CVE_SEARCH_API_INT_URL }}' - prod: - target: '{{ $env.CVE_SEARCH_API_PROD_URL }}' - phases: - - duration: 30 - arrivalRate: 1 - rampTo: 3 - maxVusers: 10 - name: 'warm up' - - duration: 60 - arrivalRate: 3 - maxVusers: 15 - name: 'sustain' - plugins: - metrics-by-endpoint: - stripQueryString: true - useOnlyRequestNames: true - metricsNamespace: 'cve-search-api.endpoint' - expect: - reportFailuresAsErrors: true - outputFormat: 'silent' - ensure: - p95: 3000 - p99: 8000 - maxErrorRate: 1 - http: - defaults: - headers: - Accept: 'application/json' - Content-Type: 'application/json' - -scenarios: - - name: 'health check' - weight: 10 - flow: - - get: - name: 'GET /api/health-check' - url: '/api/health-check' - expect: - - statusCode: 200 - - - name: 'keyword search' - weight: 35 - flow: - - post: - name: 'POST /search keyword' - url: '/search' - json: - keywordSearch: 'apache' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'multiple CVE IDs' - weight: 15 - flow: - - post: - name: 'POST /search cveIds' - url: '/search' - json: - cveIds: 'CVE-2024-0001,CVE-2024-0002' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'exclude rejected CVEs' - weight: 15 - flow: - - post: - name: 'POST /search noRejected' - url: '/search' - json: - noRejected: true - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'web search' - weight: 15 - flow: - - post: - name: 'POST /webSearch keyword' - url: '/webSearch' - json: - query: 'apache' - sort: - property: 'cveId' - order: 'asc' - from: 0 - size: 5 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'CPE name search' - weight: 5 - flow: - - post: - name: 'POST /search cpeName' - url: '/search' - json: - cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'virtual match string range' - weight: 5 - flow: - - post: - name: 'POST /search virtualMatchString range' - url: '/search' - json: - virtualMatchString: 'cpe:2.3:o:linux:linux_kernel' - versionStart: '2.2' - versionStartType: 'including' - versionEnd: '2.6' - versionEndType: 'excluding' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' diff --git a/test/artillery/smoke.yml b/test/artillery/smoke.yml deleted file mode 100644 index 364fe5f..0000000 --- a/test/artillery/smoke.yml +++ /dev/null @@ -1,128 +0,0 @@ -config: - target: 'http://127.0.0.1:4000' - environments: - local: - target: 'http://127.0.0.1:4000' - dev: - target: '{{ $env.CVE_SEARCH_API_DEV_URL }}' - test: - target: '{{ $env.CVE_SEARCH_API_TEST_URL }}' - int: - target: '{{ $env.CVE_SEARCH_API_INT_URL }}' - prod: - target: '{{ $env.CVE_SEARCH_API_PROD_URL }}' - phases: - - duration: 1 - arrivalCount: 1 - name: 'single-user smoke' - plugins: - metrics-by-endpoint: - stripQueryString: true - useOnlyRequestNames: true - metricsNamespace: 'cve-search-api.endpoint' - expect: - reportFailuresAsErrors: true - outputFormat: 'prettyError' - ensure: - p95: 5000 - maxErrorRate: 0 - http: - defaults: - headers: - Accept: 'application/json' - Content-Type: 'application/json' - -scenarios: - - name: 'API smoke checks' - flow: - - get: - name: 'GET /api/health-check' - url: '/api/health-check' - expect: - - statusCode: 200 - - - get: - name: 'GET /api-docs/openapi.json' - url: '/api-docs/openapi.json' - expect: - - statusCode: 200 - - contentType: 'json' - - - post: - name: 'POST /search keyword' - url: '/search' - json: - keywordSearch: 'apache' - resultsPerPage: 1 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' - - - post: - name: 'POST /search cveIds' - url: '/search' - json: - cveIds: 'CVE-2024-0001,CVE-2024-0002' - resultsPerPage: 2 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' - - - post: - name: 'POST /search noRejected' - url: '/search' - json: - noRejected: true - resultsPerPage: 1 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' - - - post: - name: 'POST /search cpeName' - url: '/search' - json: - cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*' - resultsPerPage: 1 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' - - - post: - name: 'POST /search virtualMatchString range' - url: '/search' - json: - virtualMatchString: 'cpe:2.3:o:linux:linux_kernel' - versionStart: '2.2' - versionStartType: 'including' - versionEnd: '2.6' - versionEndType: 'excluding' - resultsPerPage: 1 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' - - - post: - name: 'POST /webSearch keyword' - url: '/webSearch' - json: - query: 'apache' - sort: - property: 'cveId' - order: 'asc' - from: 0 - size: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - hasProperty: 'data' diff --git a/test/artillery/stress.yml b/test/artillery/stress.yml deleted file mode 100644 index 997635d..0000000 --- a/test/artillery/stress.yml +++ /dev/null @@ -1,151 +0,0 @@ -config: - target: 'http://127.0.0.1:4000' - environments: - local: - target: 'http://127.0.0.1:4000' - dev: - target: '{{ $env.CVE_SEARCH_API_DEV_URL }}' - test: - target: '{{ $env.CVE_SEARCH_API_TEST_URL }}' - int: - target: '{{ $env.CVE_SEARCH_API_INT_URL }}' - prod: - target: '{{ $env.CVE_SEARCH_API_PROD_URL }}' - phases: - - duration: 60 - arrivalRate: 2 - rampTo: 10 - maxVusers: 60 - name: 'ramp to public traffic' - - duration: 120 - arrivalRate: 10 - maxVusers: 120 - name: 'sustain public traffic' - - duration: 60 - arrivalRate: 10 - rampTo: 25 - maxVusers: 180 - name: 'stress ramp' - - duration: 60 - arrivalRate: 25 - maxVusers: 220 - name: 'stress sustain' - plugins: - metrics-by-endpoint: - stripQueryString: true - useOnlyRequestNames: true - metricsNamespace: 'cve-search-api.endpoint' - expect: - reportFailuresAsErrors: true - outputFormat: 'silent' - ensure: - p95: 8000 - p99: 15000 - maxErrorRate: 5 - http: - timeout: 20 - defaults: - headers: - Accept: 'application/json' - Content-Type: 'application/json' - -scenarios: - - name: 'health check' - weight: 5 - flow: - - get: - name: 'GET /api/health-check' - url: '/api/health-check' - expect: - - statusCode: 200 - - - name: 'website keyword search' - weight: 35 - flow: - - post: - name: 'POST /search keyword' - url: '/search' - json: - keywordSearch: 'apache' - resultsPerPage: 10 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'website webSearch' - weight: 25 - flow: - - post: - name: 'POST /webSearch keyword' - url: '/webSearch' - json: - query: 'apache' - sort: - property: 'cveId' - order: 'asc' - from: 0 - size: 10 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'multiple CVE IDs' - weight: 15 - flow: - - post: - name: 'POST /search cveIds' - url: '/search' - json: - cveIds: 'CVE-2024-0001,CVE-2024-0002' - resultsPerPage: 2 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'exclude rejected CVEs' - weight: 10 - flow: - - post: - name: 'POST /search noRejected' - url: '/search' - json: - noRejected: true - resultsPerPage: 10 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'CPE name search' - weight: 5 - flow: - - post: - name: 'POST /search cpeName' - url: '/search' - json: - cpeName: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' - - - name: 'expensive virtual match string range' - weight: 5 - flow: - - post: - name: 'POST /search virtualMatchString range' - url: '/search' - json: - virtualMatchString: 'cpe:2.3:o:linux:linux_kernel' - versionStart: '2.2' - versionStartType: 'including' - versionEnd: '2.6' - versionEndType: 'excluding' - resultsPerPage: 5 - pageNumber: 1 - expect: - - statusCode: 200 - - contentType: 'json' From a468db8fadf144cce6a9e74496d1042111ed8783 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Fri, 24 Jul 2026 10:56:20 -0400 Subject: [PATCH 02/20] Add pull request quality workflow --- .github/workflows/quality.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/quality.yml diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 0000000..95eff65 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,33 @@ +name: Search API Quality + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: search-api-quality-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + quality: + name: Search API Quality + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.14.1 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run quality checks + run: npm run quality From cfdbb658db819d60ab8732cfec8683382eb94061 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 27 Jul 2026 13:26:46 -0400 Subject: [PATCH 03/20] Bound search queue and OpenSearch deadlines --- .env.example | 3 + README.md | 10 +- api-docs/openapi.json | 9 ++ config/custom-environment-variables.jsonc | 5 +- config/default.jsonc | 8 +- config/runtime-config.test.js | 7 + controllers/searchController.js | 26 ++-- controllers/searchController.test.js | 158 ++++++++++++++++++++++ routes/index.js | 9 ++ routes/search.integration.test.js | 1 + utils/constants.js | 6 + utils/errors.js | 8 ++ utils/errors.test.js | 16 +++ utils/searchDeadline.js | 35 +++++ utils/searchDeadline.test.js | 59 ++++++++ utils/searchPostFilterWorkQueue.js | 45 +++++- utils/searchPostFilterWorkQueue.test.js | 62 ++++++++- utils/searchProvider.js | 54 +++++++- utils/searchProvider.test.js | 58 +++++++- 19 files changed, 547 insertions(+), 32 deletions(-) create mode 100644 utils/searchDeadline.js create mode 100644 utils/searchDeadline.test.js diff --git a/.env.example b/.env.example index 4cc1db0..d72ba1f 100644 --- a/.env.example +++ b/.env.example @@ -7,4 +7,7 @@ SearchPostFilterCacheTtlMs=60000 SearchPostFilterCacheMaxEntries=100 SearchPostFilterColdSearchConcurrency=10 SearchPostFilterColdSearchMaxQueueDepth=500 +SearchPostFilterColdSearchMaxWaitMs=10000 +SearchOpenSearchRequestTimeoutMs=20000 +SearchRequestDeadlineMs=30000 NPM_CA_CERT= diff --git a/README.md b/README.md index 45c1d67..0a3418b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ Rename `.env.example` to `.env` and replace or check the following values. - `SearchPostFilterCacheMaxEntries` - optional per-process CPE post-filter cache entry limit, default `100` - `SearchPostFilterColdSearchConcurrency` - optional per-process concurrent cold CPE candidate search limit, default `10` - `SearchPostFilterColdSearchMaxQueueDepth` - optional per-process waiting queue depth for cold CPE candidate searches, default `500` +- `SearchPostFilterColdSearchMaxWaitMs` - optional maximum cold CPE candidate queue wait, default `10000` +- `SearchOpenSearchRequestTimeoutMs` - optional timeout for each OpenSearch request, default `20000` +- `SearchRequestDeadlineMs` - optional overall `/search` processing deadline, default `30000` - `NPM_CA_CERT` - optional absolute host path to a corporate or self-signed CA certificate bundle used only during local Docker Compose builds ```bash @@ -216,10 +219,15 @@ The cache is intentionally small and short-lived: - In-flight behavior: concurrent identical cold searches share one candidate query per process - Cold-search concurrency: cold CPE candidate queries are limited to 10 per process before reaching OpenSearch - Cold-search queue depth: up to 500 unique cold searches can wait per process behind the concurrency limit +- Cold-search queue wait: up to 10 seconds before a waiting search returns HTTP 429 +- OpenSearch request timeout: 20 seconds for each operation +- Overall `/search` deadline: 30 seconds - Eviction: oldest entries are removed when the cache exceeds its max size When the cold-search queue is full, new unique CPE post-filter searches fail closed with HTTP 429 and a `SEARCH_QUEUE_FULL` response. Identical requests for a search that is already in flight still wait on the shared in-flight result and do not consume additional queue slots. +When a cold search waits in the queue for more than 10 seconds, it fails closed with HTTP 429 and a `SEARCH_QUEUE_TIMEOUT` response. OpenSearch operations use one attempt and return a sanitized HTTP 504 `SEARCH_TIMEOUT` response when they exceed 20 seconds. The same response is returned when a `/search` request exceeds the 30-second overall processing deadline. + Healthy dev timing logs showed the impact for repeated identical `virtualMatchString` range searches: | Metric | Cold Cache | Warm Cache | @@ -235,7 +243,7 @@ Healthy dev timing logs showed the impact for repeated identical `virtualMatchSt This helps most when multiple users submit the same expensive CPE search within the TTL. The first request is a cold cache miss; later identical requests on the same running task are warm cache hits. If identical cold requests arrive concurrently before the first one finishes, the later requests wait on the same candidate query instead of starting duplicate 10,000-candidate OpenSearch requests. Each request still fetches its requested page of full CVE records after the shared candidate result is available. -The cache does not share entries across ECS tasks. With multiple tasks, each task warms independently depending on load balancer routing. Steady repeated traffic can warm every active task, but unique one-off searches still pay the cold-cache cost. The per-process TTL, entry limit, cold-search concurrency, and cold-search queue depth can be tuned with `SearchPostFilterCacheTtlMs`, `SearchPostFilterCacheMaxEntries`, `SearchPostFilterColdSearchConcurrency`, and `SearchPostFilterColdSearchMaxQueueDepth`. Future scale improvements could add a shared cache such as Redis/ElastiCache. +The cache does not share entries across ECS tasks. With multiple tasks, each task warms independently depending on load balancer routing. Steady repeated traffic can warm every active task, but unique one-off searches still pay the cold-cache cost. The per-process TTL, entry limit, cold-search concurrency, queue depth, queue wait, OpenSearch timeout, and overall request deadline can be tuned with `SearchPostFilterCacheTtlMs`, `SearchPostFilterCacheMaxEntries`, `SearchPostFilterColdSearchConcurrency`, `SearchPostFilterColdSearchMaxQueueDepth`, `SearchPostFilterColdSearchMaxWaitMs`, `SearchOpenSearchRequestTimeoutMs`, and `SearchRequestDeadlineMs`. Future scale improvements could add a shared cache such as Redis/ElastiCache. Docker runtime smoke checks can be run after building the image. The default `Dockerfile` avoids BuildKit `RUN --mount` syntax for AWS builders that do not support it: diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 7db0faf..754245a 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -32,6 +32,12 @@ }, "404": { "description": "Invalid cpeName value" + }, + "429": { + "description": "Cold CPE search queue is full or exceeded its maximum wait" + }, + "504": { + "description": "Search exceeded its OpenSearch or overall processing deadline" } }, "requestBody": { @@ -133,6 +139,9 @@ }, "400": { "description": "Bad Request" + }, + "504": { + "description": "Search exceeded its OpenSearch request deadline" } }, "requestBody": { diff --git a/config/custom-environment-variables.jsonc b/config/custom-environment-variables.jsonc index 9dc44fc..814ed47 100644 --- a/config/custom-environment-variables.jsonc +++ b/config/custom-environment-variables.jsonc @@ -15,7 +15,10 @@ "postFilterCacheTtlMs": "SearchPostFilterCacheTtlMs", "postFilterCacheMaxEntries": "SearchPostFilterCacheMaxEntries", "postFilterColdSearchConcurrency": "SearchPostFilterColdSearchConcurrency", - "postFilterColdSearchMaxQueueDepth": "SearchPostFilterColdSearchMaxQueueDepth" + "postFilterColdSearchMaxQueueDepth": "SearchPostFilterColdSearchMaxQueueDepth", + "postFilterColdSearchMaxWaitMs": "SearchPostFilterColdSearchMaxWaitMs", + "openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs", + "requestDeadlineMs": "SearchRequestDeadlineMs" }, // constants for testing node-config // these values are only used to test node-config in AppConfig.test.int.ts diff --git a/config/default.jsonc b/config/default.jsonc index 136c718..da11376 100644 --- a/config/default.jsonc +++ b/config/default.jsonc @@ -22,7 +22,13 @@ // Limits concurrent cold CPE candidate OpenSearch requests per Node.js process. "postFilterColdSearchConcurrency": "10", // Limits queued cold CPE candidate searches waiting behind the concurrency cap per Node.js process. - "postFilterColdSearchMaxQueueDepth": "500" + "postFilterColdSearchMaxQueueDepth": "500", + // Limits how long a cold CPE candidate search can wait in the per-process queue. + "postFilterColdSearchMaxWaitMs": "10000", + // Limits each request from the API process to OpenSearch. + "openSearchRequestTimeoutMs": "20000", + // Limits total processing time for a /search request. + "requestDeadlineMs": "30000" } } } diff --git a/config/runtime-config.test.js b/config/runtime-config.test.js index 24d9b3d..a023610 100644 --- a/config/runtime-config.test.js +++ b/config/runtime-config.test.js @@ -95,6 +95,13 @@ describe('runtime container and compose configuration', () => { expect(customEnvironmentVariables).toContain( '"postFilterColdSearchMaxQueueDepth": "SearchPostFilterColdSearchMaxQueueDepth"' ); + expect(customEnvironmentVariables).toContain( + '"postFilterColdSearchMaxWaitMs": "SearchPostFilterColdSearchMaxWaitMs"' + ); + expect(customEnvironmentVariables).toContain( + '"openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs"' + ); + expect(customEnvironmentVariables).toContain('"requestDeadlineMs": "SearchRequestDeadlineMs"'); }); test.each([['config/default.jsonc'], ['config/devel.jsonc'], ['config/prod.jsonc']])( diff --git a/controllers/searchController.js b/controllers/searchController.js index b94448d..0c14203 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -2,6 +2,7 @@ const PackageInfo = require('cve-core').packageInfo; const errors = require('../utils/errors'); const cpe = require('../utils/cpe'); const searchPerformance = require('../utils/searchPerformance'); +const searchDeadline = require('../utils/searchDeadline'); const searchPostFilterCache = require('../utils/searchPostFilterCache'); const searchPostFilterWorkQueue = require('../utils/searchPostFilterWorkQueue'); const searchProvider = require('../utils/searchProvider'); @@ -329,13 +330,9 @@ async function searchHandler(req, res, next) { const { searchOptions, queryStringObjects, postFilters, responseOptions } = searchQueryBuilder.buildSearchRequest(params); - const resp = await executeSearch( - searchManager, - searchOptions, - queryStringObjects, - postFilters, - responseOptions, - metrics + const resp = await searchDeadline.withSearchDeadline( + executeSearch(searchManager, searchOptions, queryStringObjects, postFilters, responseOptions, metrics), + searchProvider.getSearchRequestDeadlineMs() ); const pagingOptions = responseOptions ?? searchOptions; metrics.totalElapsedMs = searchPerformance.elapsedMs(handlerStart); @@ -360,6 +357,16 @@ async function searchHandler(req, res, next) { return res.status(429).json(error.searchQueueFullError()); } + if (searchPostFilterWorkQueue.isQueueTimeoutError(err)) { + if (typeof res.set === 'function') res.set('Retry-After', '5'); + + return res.status(429).json(error.searchQueueTimeoutError()); + } + + if (searchDeadline.isSearchTimeoutError(err)) { + return res.status(504).json(error.searchTimeoutError()); + } + if (err.name === 'ConnectionError') { return res.status(400).json(error.openSearchError()); } @@ -420,10 +427,11 @@ async function webSearchHandler(req, res, next) { body.searchMetadata.errors = [error.upstreamSearchError()]; } } catch (ex) { - let err = error.upstreamSearchError(); + const timedOut = searchDeadline.isSearchTimeoutError(ex); + const err = timedOut ? error.searchTimeoutError() : error.upstreamSearchError(); body.searchMetadata.errors = [err]; - return res.status(502).json(body); + return res.status(timedOut ? 504 : 502).json(body); } return res.status(status).json(body); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 852d993..28a1f93 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -6,6 +6,8 @@ const mockGetPostFilterCacheTtlMs = jest.fn(() => 60000); const mockGetPostFilterCacheMaxEntries = jest.fn(() => 100); const mockGetPostFilterColdSearchConcurrency = jest.fn(() => 10); const mockGetPostFilterColdSearchMaxQueueDepth = jest.fn(() => 500); +const mockGetPostFilterColdSearchMaxWaitMs = jest.fn(() => 10000); +const mockGetSearchRequestDeadlineMs = jest.fn(() => 30000); const mockGetAdvancedSearchManager = jest.fn(() => ({ apiSearch: mockApiSearch })); @@ -28,6 +30,8 @@ jest.mock('../utils/searchProvider', () => ({ getPostFilterCacheTtlMs: mockGetPostFilterCacheTtlMs, getPostFilterColdSearchConcurrency: mockGetPostFilterColdSearchConcurrency, getPostFilterColdSearchMaxQueueDepth: mockGetPostFilterColdSearchMaxQueueDepth, + getPostFilterColdSearchMaxWaitMs: mockGetPostFilterColdSearchMaxWaitMs, + getSearchRequestDeadlineMs: mockGetSearchRequestDeadlineMs, searchRaw: mockSearchRaw, shouldLogSearchPerformance: mockShouldLogSearchPerformance })); @@ -86,6 +90,24 @@ function deferred() { return state; } +function captureTimers() { + const timers = []; + jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay) => { + const timer = { + unref: jest.fn() + }; + timers.push({ + callback, + delay, + timer + }); + return timer; + }); + jest.spyOn(global, 'clearTimeout').mockImplementation(() => {}); + + return timers; +} + function cpeHit(id, cpeMatch) { return { _id: id, @@ -120,10 +142,14 @@ describe('Search controller SEARCH handler', () => { mockGetPostFilterCacheMaxEntries.mockClear(); mockGetPostFilterColdSearchConcurrency.mockClear(); mockGetPostFilterColdSearchMaxQueueDepth.mockClear(); + mockGetPostFilterColdSearchMaxWaitMs.mockClear(); + mockGetSearchRequestDeadlineMs.mockClear(); mockGetPostFilterCacheTtlMs.mockReturnValue(60000); mockGetPostFilterCacheMaxEntries.mockReturnValue(100); mockGetPostFilterColdSearchConcurrency.mockReturnValue(10); mockGetPostFilterColdSearchMaxQueueDepth.mockReturnValue(500); + mockGetPostFilterColdSearchMaxWaitMs.mockReturnValue(10000); + mockGetSearchRequestDeadlineMs.mockReturnValue(30000); searchPostFilterCache.clear(); jest.spyOn(console, 'log').mockImplementation(() => {}); @@ -768,6 +794,57 @@ describe('Search controller SEARCH handler', () => { await firstSearch; }); + test('SEARCH returns 429 when a cold semantic post-filter search exceeds the queue wait deadline', async () => { + const timers = captureTimers(); + const candidateSearch = deferred(); + const firstRes = createResponse(); + const secondRes = createResponse(); + + mockGetPostFilterColdSearchConcurrency.mockReturnValue(1); + mockGetPostFilterColdSearchMaxQueueDepth.mockReturnValue(1); + mockGetPostFilterColdSearchMaxWaitMs.mockReturnValue(10000); + mockSearchRaw.mockReturnValue(candidateSearch.promise); + + const firstSearch = controller.SEARCH( + { + query: { + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + resultsPerPage: '1', + pageNumber: '1' + } + }, + firstRes, + next + ); + await Promise.resolve(); + + const secondSearch = controller.SEARCH( + { + query: { + virtualMatchString: 'cpe:2.3:o:microsoft:windows_10_1809', + resultsPerPage: '1', + pageNumber: '1' + } + }, + secondRes, + next + ); + await Promise.resolve(); + + timers.find(({ delay }) => delay === 10000).callback(); + await secondSearch; + + expect(secondRes.set).toHaveBeenCalledWith('Retry-After', '5'); + expect(secondRes.status).toHaveBeenCalledWith(429); + expect(secondRes.json).toHaveBeenCalledWith({ + error: 'SEARCH_QUEUE_TIMEOUT', + message: 'Search service is busy. Please retry later.' + }); + + candidateSearch.resolve(rawResponse(0, [])); + await firstSearch; + }); + test('SEARCH returns sanitized upstream errors when optimized post-filter search fails', async () => { mockSearchRaw.mockRejectedValue(new Error('candidate search failed')); @@ -1107,6 +1184,51 @@ describe('Search controller SEARCH handler', () => { }); }); + test('SEARCH returns a sanitized 504 when OpenSearch times out', async () => { + mockApiSearch.mockRejectedValue({ + name: 'TimeoutError', + message: 'sensitive upstream timeout details' + }); + + await controller.SEARCH( + { + query: {} + }, + res, + next + ); + + expect(res.status).toHaveBeenCalledWith(504); + expect(res.json).toHaveBeenCalledWith({ + error: 'SEARCH_TIMEOUT', + message: 'Search request timed out. Please try again later.' + }); + }); + + test('SEARCH returns a sanitized 504 when the overall request deadline is exceeded', async () => { + const timers = captureTimers(); + mockApiSearch.mockReturnValue(new Promise(() => {})); + + const search = controller.SEARCH( + { + query: {} + }, + res, + next + ); + await Promise.resolve(); + + timers.find(({ delay }) => delay === 30000).callback(); + await search; + + expect(mockGetSearchRequestDeadlineMs).toHaveBeenCalledTimes(1); + expect(res.status).toHaveBeenCalledWith(504); + expect(res.json).toHaveBeenCalledWith({ + error: 'SEARCH_TIMEOUT', + message: 'Search request timed out. Please try again later.' + }); + }); + test('SEARCH returns sanitized errors from cve-core', async () => { const err = { name: 'SearchError', @@ -1393,4 +1515,40 @@ describe('Search controller WEB_SEARCH handler', () => { }); expect(console.log).not.toHaveBeenCalled(); }); + + test('WEB_SEARCH returns a sanitized 504 when OpenSearch times out', async () => { + mockBasicSearch.mockRejectedValue({ + name: 'TimeoutError', + message: 'sensitive upstream timeout details' + }); + + await controller.WEB_SEARCH( + { + body: { + query: 'apache' + } + }, + res, + next + ); + + expect(res.status).toHaveBeenCalledWith(504); + expect(res.json.mock.calls[0][0]).toStrictEqual({ + searchMetadata: { + searchStatus: 'errors', + version: '2025-08-26', + cveCore: 'test-core-version', + cveMitreCore: 'test-core-version', + notes: [], + errors: [ + { + error: 'SEARCH_TIMEOUT', + message: 'Search request timed out. Please try again later.' + } + ] + }, + resultsTotal: 0, + data: [] + }); + }); }); diff --git a/routes/index.js b/routes/index.js index bab6f82..0264af1 100644 --- a/routes/index.js +++ b/routes/index.js @@ -190,6 +190,12 @@ description: 'A list of matching CVE records', #swagger.responses[404] = { description: 'Invalid cpeName value', } +#swagger.responses[429] = { +description: 'Cold CPE search queue is full or exceeded its maximum wait', +} +#swagger.responses[504] = { +description: 'Search exceeded its OpenSearch or overall processing deadline', +} */ rejectSearchQueryParams, body().custom(isSearchBodyObject).withMessage(error.searchBodyError()), @@ -289,6 +295,9 @@ router.post( #swagger.responses[200] = { description: 'A list of matching CVE records', } +#swagger.responses[504] = { +description: 'Search exceeded its OpenSearch request deadline', +} */ body().custom(hasOnlyWebSearchBodyFields).withMessage(error.webSearchUnsupportedFieldError()), body('query') diff --git a/routes/search.integration.test.js b/routes/search.integration.test.js index 9b671b4..740f9ad 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -10,6 +10,7 @@ jest.mock('../utils/searchProvider', () => ({ getBasicSearchManager: jest.fn(() => ({ search: mockBasicSearch })), + getSearchRequestDeadlineMs: jest.fn(() => 30000), shouldLogSearchPerformance: jest.fn(() => false) })); diff --git a/utils/constants.js b/utils/constants.js index b6c7696..6a98784 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -9,6 +9,9 @@ const SEARCH_POST_FILTER_CACHE_TTL_MS = 60 * 1000; const SEARCH_POST_FILTER_CACHE_MAX_ENTRIES = 100; const SEARCH_POST_FILTER_COLD_SEARCH_CONCURRENCY = 10; const SEARCH_POST_FILTER_COLD_SEARCH_MAX_QUEUE_DEPTH = 500; +const SEARCH_POST_FILTER_COLD_SEARCH_MAX_WAIT_MS = 10 * 1000; +const SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS = 20 * 1000; +const SEARCH_REQUEST_DEADLINE_MS = 30 * 1000; const rejectedCveStates = ['REJECT', 'REJECTED', 'Rejected']; const rejectedCveStateFields = ['cveMetadata.state', 'containers.cna.x_legacyV4Record.CVE_data_meta.STATE']; @@ -126,6 +129,9 @@ module.exports = { SEARCH_POST_FILTER_CACHE_MAX_ENTRIES, SEARCH_POST_FILTER_COLD_SEARCH_CONCURRENCY, SEARCH_POST_FILTER_COLD_SEARCH_MAX_QUEUE_DEPTH, + SEARCH_POST_FILTER_COLD_SEARCH_MAX_WAIT_MS, + SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS, + SEARCH_REQUEST_DEADLINE_MS, RESULTS_PER_PAGE, REJECTED_CVE_STATES: rejectedCveStates, CPE_VERSION_RANGE_TYPES: cpeVersionRangeTypes, diff --git a/utils/errors.js b/utils/errors.js index ca612c0..3640826 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -243,6 +243,14 @@ class errors { searchQueueFullError() { return makeError('SEARCH_QUEUE_FULL', 'Search service is busy. Please retry later.'); } + + searchQueueTimeoutError() { + return makeError('SEARCH_QUEUE_TIMEOUT', 'Search service is busy. Please retry later.'); + } + + searchTimeoutError() { + return makeError('SEARCH_TIMEOUT', 'Search request timed out. Please try again later.'); + } } module.exports = { diff --git a/utils/errors.test.js b/utils/errors.test.js index f1a0206..23f3b15 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -174,6 +174,22 @@ describe('error response helpers', () => { message: 'Search service is busy. Please retry later.' } ], + [ + 'searchQueueTimeoutError', + [], + { + error: 'SEARCH_QUEUE_TIMEOUT', + message: 'Search service is busy. Please retry later.' + } + ], + [ + 'searchTimeoutError', + [], + { + error: 'SEARCH_TIMEOUT', + message: 'Search request timed out. Please try again later.' + } + ], [ 'searchBodyError', [], diff --git a/utils/searchDeadline.js b/utils/searchDeadline.js new file mode 100644 index 0000000..e0fafc0 --- /dev/null +++ b/utils/searchDeadline.js @@ -0,0 +1,35 @@ +class SearchDeadlineExceededError extends Error { + constructor() { + super('Search request deadline exceeded.'); + this.name = 'SearchDeadlineExceededError'; + this.code = 'SEARCH_TIMEOUT'; + } +} + +function isSearchTimeoutError(err) { + return err instanceof SearchDeadlineExceededError || err?.code === 'SEARCH_TIMEOUT' || err?.name === 'TimeoutError'; +} + +async function withSearchDeadline(promise, timeoutMs) { + const startedAt = Date.now(); + let timeoutId; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new SearchDeadlineExceededError()), timeoutMs); + timeoutId.unref?.(); + }); + + try { + const result = await Promise.race([promise, timeoutPromise]); + if (Date.now() - startedAt >= timeoutMs) throw new SearchDeadlineExceededError(); + + return result; + } finally { + clearTimeout(timeoutId); + } +} + +module.exports = { + isSearchTimeoutError, + SearchDeadlineExceededError, + withSearchDeadline +}; diff --git a/utils/searchDeadline.test.js b/utils/searchDeadline.test.js new file mode 100644 index 0000000..944cb80 --- /dev/null +++ b/utils/searchDeadline.test.js @@ -0,0 +1,59 @@ +const searchDeadline = require('./searchDeadline'); + +function captureTimers() { + const timers = []; + jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay) => { + const timer = { + unref: jest.fn() + }; + timers.push({ + callback, + delay, + timer + }); + return timer; + }); + jest.spyOn(global, 'clearTimeout').mockImplementation(() => {}); + + return timers; +} + +describe('searchDeadline', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + test('returns a search result completed before the deadline', async () => { + await expect(searchDeadline.withSearchDeadline(Promise.resolve('result'), 30000)).resolves.toBe('result'); + }); + + test('rejects a search that exceeds the deadline', async () => { + const timers = captureTimers(); + const pendingSearch = new Promise(() => {}); + const result = searchDeadline.withSearchDeadline(pendingSearch, 30000); + const timeoutExpectation = expect(result).rejects.toMatchObject({ + name: 'SearchDeadlineExceededError', + code: 'SEARCH_TIMEOUT' + }); + + timers.find(({ delay }) => delay === 30000).callback(); + + await timeoutExpectation; + }); + + test('rejects work that completes after the deadline before the timer can run', async () => { + jest.spyOn(Date, 'now').mockReturnValueOnce(0).mockReturnValueOnce(30001); + + await expect(searchDeadline.withSearchDeadline(Promise.resolve('late result'), 30000)).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + }); + + test.each([ + [new searchDeadline.SearchDeadlineExceededError()], + [{ code: 'SEARCH_TIMEOUT' }], + [{ name: 'TimeoutError' }] + ])('recognizes search timeout error %#', (err) => { + expect(searchDeadline.isSearchTimeoutError(err)).toBe(true); + }); +}); diff --git a/utils/searchPostFilterWorkQueue.js b/utils/searchPostFilterWorkQueue.js index 704efcf..27c2dd3 100644 --- a/utils/searchPostFilterWorkQueue.js +++ b/utils/searchPostFilterWorkQueue.js @@ -12,6 +12,15 @@ class SearchPostFilterQueueFullError extends Error { } } +class SearchPostFilterQueueTimeoutError extends Error { + constructor(state) { + super('CPE post-filter cold search queue wait timed out.'); + this.name = 'SearchPostFilterQueueTimeoutError'; + this.code = 'SEARCH_QUEUE_TIMEOUT'; + this.state = state; + } +} + function getMaxConcurrent() { return searchProvider.getPostFilterColdSearchConcurrency(); } @@ -20,12 +29,17 @@ function getMaxQueueDepth() { return searchProvider.getPostFilterColdSearchMaxQueueDepth(); } +function getMaxWaitMs() { + return searchProvider.getPostFilterColdSearchMaxWaitMs(); +} + function getState() { return { activeCount, queuedCount: queue.length, maxConcurrent: getMaxConcurrent(), - maxQueueDepth: getMaxQueueDepth() + maxQueueDepth: getMaxQueueDepth(), + maxWaitMs: getMaxWaitMs() }; } @@ -33,11 +47,16 @@ function isQueueFullError(err) { return err instanceof SearchPostFilterQueueFullError || err?.code === 'SEARCH_QUEUE_FULL'; } +function isQueueTimeoutError(err) { + return err instanceof SearchPostFilterQueueTimeoutError || err?.code === 'SEARCH_QUEUE_TIMEOUT'; +} + function drain() { const maxConcurrent = getMaxConcurrent(); while (activeCount < maxConcurrent && queue.length) { const item = queue.shift(); + clearTimeout(item.timeoutId); activeCount += 1; void (async () => { @@ -60,24 +79,42 @@ function run(task) { return; } - queue.push({ + const item = { task, resolve, - reject - }); + reject, + timeoutId: undefined + }; + + if (activeCount >= getMaxConcurrent()) { + item.timeoutId = setTimeout(() => { + const queueIndex = queue.indexOf(item); + if (queueIndex === -1) return; + + queue.splice(queueIndex, 1); + reject(new SearchPostFilterQueueTimeoutError(getState())); + drain(); + }, getMaxWaitMs()); + item.timeoutId.unref?.(); + } + + queue.push(item); drain(); }); } function resetForTests() { activeCount = 0; + queue.forEach((item) => clearTimeout(item.timeoutId)); queue.length = 0; } module.exports = { SearchPostFilterQueueFullError, + SearchPostFilterQueueTimeoutError, getState, isQueueFullError, + isQueueTimeoutError, resetForTests, run }; diff --git a/utils/searchPostFilterWorkQueue.test.js b/utils/searchPostFilterWorkQueue.test.js index ea2ff89..bea5751 100644 --- a/utils/searchPostFilterWorkQueue.test.js +++ b/utils/searchPostFilterWorkQueue.test.js @@ -1,9 +1,11 @@ const mockGetPostFilterColdSearchConcurrency = jest.fn(); const mockGetPostFilterColdSearchMaxQueueDepth = jest.fn(); +const mockGetPostFilterColdSearchMaxWaitMs = jest.fn(); jest.mock('./searchProvider', () => ({ getPostFilterColdSearchConcurrency: mockGetPostFilterColdSearchConcurrency, - getPostFilterColdSearchMaxQueueDepth: mockGetPostFilterColdSearchMaxQueueDepth + getPostFilterColdSearchMaxQueueDepth: mockGetPostFilterColdSearchMaxQueueDepth, + getPostFilterColdSearchMaxWaitMs: mockGetPostFilterColdSearchMaxWaitMs })); const searchPostFilterWorkQueue = require('./searchPostFilterWorkQueue'); @@ -17,13 +19,38 @@ function deferred() { return state; } +function captureTimers() { + const timers = []; + jest.spyOn(global, 'setTimeout').mockImplementation((callback, delay) => { + const timer = { + unref: jest.fn() + }; + timers.push({ + callback, + delay, + timer + }); + return timer; + }); + jest.spyOn(global, 'clearTimeout').mockImplementation(() => {}); + + return timers; +} + describe('searchPostFilterWorkQueue', () => { beforeEach(() => { searchPostFilterWorkQueue.resetForTests(); mockGetPostFilterColdSearchConcurrency.mockReset(); mockGetPostFilterColdSearchMaxQueueDepth.mockReset(); + mockGetPostFilterColdSearchMaxWaitMs.mockReset(); mockGetPostFilterColdSearchConcurrency.mockReturnValue(1); mockGetPostFilterColdSearchMaxQueueDepth.mockReturnValue(1); + mockGetPostFilterColdSearchMaxWaitMs.mockReturnValue(10000); + }); + + afterEach(() => { + searchPostFilterWorkQueue.resetForTests(); + jest.restoreAllMocks(); }); test('runs no more than the configured number of cold searches concurrently', async () => { @@ -42,7 +69,8 @@ describe('searchPostFilterWorkQueue', () => { activeCount: 1, queuedCount: 1, maxConcurrent: 1, - maxQueueDepth: 1 + maxQueueDepth: 1, + maxWaitMs: 10000 }); first.resolve('first'); @@ -81,7 +109,8 @@ describe('searchPostFilterWorkQueue', () => { activeCount: 1, queuedCount: 1, maxConcurrent: 1, - maxQueueDepth: 1 + maxQueueDepth: 1, + maxWaitMs: 10000 } }); @@ -95,4 +124,31 @@ describe('searchPostFilterWorkQueue', () => { await expect(searchPostFilterWorkQueue.run(() => 'first')).resolves.toBe('first'); }); + + test('rejects queued work that exceeds the configured maximum wait', async () => { + const timers = captureTimers(); + const first = deferred(); + const secondTask = jest.fn(() => 'second'); + + const firstRun = searchPostFilterWorkQueue.run(() => first.promise); + const secondRun = searchPostFilterWorkQueue.run(secondTask); + const timeoutExpectation = expect(secondRun).rejects.toMatchObject({ + code: 'SEARCH_QUEUE_TIMEOUT', + state: { + activeCount: 1, + queuedCount: 0, + maxConcurrent: 1, + maxQueueDepth: 1, + maxWaitMs: 10000 + } + }); + await Promise.resolve(); + + timers.find(({ delay }) => delay === 10000).callback(); + await timeoutExpectation; + expect(secondTask).not.toHaveBeenCalled(); + + first.resolve('first'); + await expect(firstRun).resolves.toBe('first'); + }); }); diff --git a/utils/searchProvider.js b/utils/searchProvider.js index 7d3952b..e056387 100644 --- a/utils/searchProvider.js +++ b/utils/searchProvider.js @@ -6,6 +6,20 @@ let advancedSearchManager; let basicSearchManager; let searchReader; +function configureClientRequestTimeout(client) { + if (client?.transport) { + client.transport.requestTimeout = getOpenSearchRequestTimeoutMs(); + client.transport.maxRetries = 0; + } + + return client; +} + +function configureManagerRequestTimeout(manager) { + configureClientRequestTimeout(manager?._searchReader?._client); + return manager; +} + function normalizeBoolean(value, defaultValue = false) { if (value === undefined || value === null || value === '') return defaultValue; if (typeof value === 'boolean') return value; @@ -63,7 +77,7 @@ function getSearchProviderConfig() { function getAdvancedSearchManager() { if (!advancedSearchManager) { - advancedSearchManager = new AdvancedSearchManager(getSearchProviderConfig()); + advancedSearchManager = configureManagerRequestTimeout(new AdvancedSearchManager(getSearchProviderConfig())); } return advancedSearchManager; @@ -71,7 +85,7 @@ function getAdvancedSearchManager() { function getBasicSearchManager() { if (!basicSearchManager) { - basicSearchManager = new BasicSearchManager(getSearchProviderConfig()); + basicSearchManager = configureManagerRequestTimeout(new BasicSearchManager(getSearchProviderConfig())); } return basicSearchManager; @@ -83,6 +97,7 @@ function getSearchReader() { searchReader = new SearchReader(config.providerEndpoint, config.index, { ignore_ssl_cert_check_errors: config.allowUnknownSslCerts }); + configureClientRequestTimeout(searchReader._client); } return searchReader; @@ -90,10 +105,16 @@ function getSearchReader() { async function searchRaw(body) { const reader = getSearchReader(); - const response = await reader._client.search({ - index: reader._cveIndex, - body - }); + const response = await reader._client.search( + { + index: reader._cveIndex, + body + }, + { + maxRetries: 0, + requestTimeout: getOpenSearchRequestTimeoutMs() + } + ); return response.body; } @@ -128,6 +149,24 @@ function getPostFilterColdSearchMaxQueueDepth() { ); } +function getPostFilterColdSearchMaxWaitMs() { + return getIntegerConfig( + 'search.postFilterColdSearchMaxWaitMs', + constants.SEARCH_POST_FILTER_COLD_SEARCH_MAX_WAIT_MS, + { min: 1 } + ); +} + +function getOpenSearchRequestTimeoutMs() { + return getIntegerConfig('search.openSearchRequestTimeoutMs', constants.SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS, { + min: 1 + }); +} + +function getSearchRequestDeadlineMs() { + return getIntegerConfig('search.requestDeadlineMs', constants.SEARCH_REQUEST_DEADLINE_MS, { min: 1 }); +} + function resetSearchProviderForTests() { searchProviderConfig = undefined; advancedSearchManager = undefined; @@ -144,6 +183,9 @@ module.exports = { getPostFilterCacheTtlMs, getPostFilterColdSearchConcurrency, getPostFilterColdSearchMaxQueueDepth, + getPostFilterColdSearchMaxWaitMs, + getOpenSearchRequestTimeoutMs, + getSearchRequestDeadlineMs, getSearchProviderConfig, getSearchReader, normalizeBoolean, diff --git a/utils/searchProvider.test.js b/utils/searchProvider.test.js index e1fcb82..56899d7 100644 --- a/utils/searchProvider.test.js +++ b/utils/searchProvider.test.js @@ -1,8 +1,24 @@ const mockAdvancedSearchManager = jest.fn(function AdvancedSearchManager(spec) { this.spec = spec; + this._searchReader = { + _client: { + transport: { + maxRetries: 3, + requestTimeout: 30000 + } + } + }; }); const mockBasicSearchManager = jest.fn(function BasicSearchManager(spec) { this.spec = spec; + this._searchReader = { + _client: { + transport: { + maxRetries: 3, + requestTimeout: 30000 + } + } + }; }); const mockClientSearch = jest.fn(); const mockSearchReader = jest.fn(function SearchReader(endpoint, index, options) { @@ -10,7 +26,11 @@ const mockSearchReader = jest.fn(function SearchReader(endpoint, index, options) this._cveIndex = index; this._options = options; this._client = { - search: mockClientSearch + search: mockClientSearch, + transport: { + maxRetries: 3, + requestTimeout: 30000 + } }; }); const mockGetDefaultSearchProviderSpec = jest.fn(); @@ -83,6 +103,8 @@ describe('searchProvider', () => { allowUnknownSslCerts: false }); expect(mockGetDefaultSearchProviderSpec).toHaveBeenCalledTimes(1); + expect(first._searchReader._client.transport.maxRetries).toBe(0); + expect(first._searchReader._client.transport.requestTimeout).toBe(20000); }); test('reuses one basic search manager per process', () => { @@ -97,6 +119,8 @@ describe('searchProvider', () => { allowUnknownSslCerts: false }); expect(mockGetDefaultSearchProviderSpec).toHaveBeenCalledTimes(1); + expect(first._searchReader._client.transport.maxRetries).toBe(0); + expect(first._searchReader._client.transport.requestTimeout).toBe(20000); }); test('reuses one search reader per process for raw OpenSearch searches', async () => { @@ -125,10 +149,18 @@ describe('searchProvider', () => { expect(mockSearchReader).toHaveBeenCalledWith('https://search.example', 'test-index', { ignore_ssl_cert_check_errors: false }); - expect(mockClientSearch).toHaveBeenCalledWith({ - index: 'test-index', - body - }); + expect(mockClientSearch).toHaveBeenCalledWith( + { + index: 'test-index', + body + }, + { + maxRetries: 0, + requestTimeout: 20000 + } + ); + expect(first._client.transport.maxRetries).toBe(0); + expect(first._client.transport.requestTimeout).toBe(20000); expect(result).toStrictEqual({ hits: { total: { @@ -187,7 +219,10 @@ describe('searchProvider', () => { 'search.postFilterCacheTtlMs', 'search.postFilterCacheMaxEntries', 'search.postFilterColdSearchConcurrency', - 'search.postFilterColdSearchMaxQueueDepth' + 'search.postFilterColdSearchMaxQueueDepth', + 'search.postFilterColdSearchMaxWaitMs', + 'search.openSearchRequestTimeoutMs', + 'search.requestDeadlineMs' ].includes(key) ); mockAppConfigGet.mockImplementation((key) => { @@ -195,7 +230,10 @@ describe('searchProvider', () => { 'search.postFilterCacheTtlMs': '120000', 'search.postFilterCacheMaxEntries': '250', 'search.postFilterColdSearchConcurrency': '6', - 'search.postFilterColdSearchMaxQueueDepth': '50' + 'search.postFilterColdSearchMaxQueueDepth': '50', + 'search.postFilterColdSearchMaxWaitMs': '15000', + 'search.openSearchRequestTimeoutMs': '25000', + 'search.requestDeadlineMs': '45000' }; return values[key]; }); @@ -204,6 +242,9 @@ describe('searchProvider', () => { expect(searchProvider.getPostFilterCacheMaxEntries()).toBe(250); expect(searchProvider.getPostFilterColdSearchConcurrency()).toBe(6); expect(searchProvider.getPostFilterColdSearchMaxQueueDepth()).toBe(50); + expect(searchProvider.getPostFilterColdSearchMaxWaitMs()).toBe(15000); + expect(searchProvider.getOpenSearchRequestTimeoutMs()).toBe(25000); + expect(searchProvider.getSearchRequestDeadlineMs()).toBe(45000); }); test('falls back to CPE post-filter runtime defaults when config values are invalid', () => { @@ -214,5 +255,8 @@ describe('searchProvider', () => { expect(searchProvider.getPostFilterCacheMaxEntries()).toBe(100); expect(searchProvider.getPostFilterColdSearchConcurrency()).toBe(10); expect(searchProvider.getPostFilterColdSearchMaxQueueDepth()).toBe(500); + expect(searchProvider.getPostFilterColdSearchMaxWaitMs()).toBe(10000); + expect(searchProvider.getOpenSearchRequestTimeoutMs()).toBe(20000); + expect(searchProvider.getSearchRequestDeadlineMs()).toBe(30000); }); }); From 3f871fd111ce0c60ae3c46410daed47dd6f61a9f Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 27 Jul 2026 13:59:04 -0400 Subject: [PATCH 04/20] Add structured search telemetry and request logging --- .env.example | 2 + README.md | 12 +- api-docs/openapi.json | 30 ++++- api-docs/openapi.test.js | 11 ++ config/custom-environment-variables.jsonc | 4 + config/default.jsonc | 7 + config/runtime-config.test.js | 2 + controllers/searchController.js | 45 ++++++- controllers/searchController.test.js | 61 +++++++++ routes/index.js | 24 ++++ server.js | 15 ++- server.test.js | 18 ++- test/http.js | 17 ++- utils/requestLogging.js | 98 ++++++++++++++ utils/requestLogging.test.js | 150 ++++++++++++++++++++++ utils/searchPerformance.js | 51 +++++--- utils/searchPerformance.test.js | 64 ++++++++- utils/searchPostFilterWorkQueue.js | 7 +- utils/searchPostFilterWorkQueue.test.js | 1 + 19 files changed, 575 insertions(+), 44 deletions(-) create mode 100644 utils/requestLogging.js create mode 100644 utils/requestLogging.test.js diff --git a/.env.example b/.env.example index d72ba1f..8371eb3 100644 --- a/.env.example +++ b/.env.example @@ -10,4 +10,6 @@ SearchPostFilterColdSearchMaxQueueDepth=500 SearchPostFilterColdSearchMaxWaitMs=10000 SearchOpenSearchRequestTimeoutMs=20000 SearchRequestDeadlineMs=30000 +RequestLogSuccessSampleRate=0.1 +RequestLogHealthChecks=false NPM_CA_CERT= diff --git a/README.md b/README.md index 0a3418b..aa43f12 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Rename `.env.example` to `.env` and replace or check the following values. - `SearchPostFilterColdSearchMaxWaitMs` - optional maximum cold CPE candidate queue wait, default `10000` - `SearchOpenSearchRequestTimeoutMs` - optional timeout for each OpenSearch request, default `20000` - `SearchRequestDeadlineMs` - optional overall `/search` processing deadline, default `30000` +- `RequestLogSuccessSampleRate` - optional fraction of successful non-health-check access logs to retain, default `0.1` +- `RequestLogHealthChecks` - optional flag to retain successful health-check access logs, default `false` - `NPM_CA_CERT` - optional absolute host path to a corporate or self-signed CA certificate bundle used only during local Docker Compose builds ```bash @@ -202,7 +204,15 @@ npm run coverage npm run quality ``` -Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. The timing log is redacted and includes request strategy, cache hit status, in-flight hit status, candidate counts, filtered counts, returned count, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. +### Operational Logging + +Every request receives an `X-Request-ID` response header. A caller-provided `X-Request-ID` is preserved when it contains only letters, numbers, `.`, `_`, `:`, or `-` and is no longer than 128 characters; otherwise the API generates a UUID. The same value appears as `request-id` in access logs and `requestId` in structured `/search` events. + +Access logs omit query strings and request bodies. By default, the API retains 10% of successful non-health-check access logs, suppresses successful `/api/health-check` access logs, and retains every HTTP 4xx or 5xx access log. Set `RequestLogSuccessSampleRate` from `0` through `1` to change successful-request sampling. Set `RequestLogHealthChecks=true` to retain successful health-check logs. + +The API always emits a sanitized JSON `search.performance` event for `/search` queue saturation, queue wait timeout, search timeout, OpenSearch connection errors, and upstream search errors. These events include the request ID, outcome, HTTP status, strategy, cache state, total elapsed time, and queue state when applicable. Search terms, CPE values, request bodies, credentials, and upstream error details are never included. + +Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. Successful requests then emit the same structured event with additional redacted timing details, including candidate counts, filtered counts, returned count, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. ### CPE Post-Filter Cache diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 754245a..dbcf6e7 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -25,7 +25,15 @@ "operationId": "search", "responses": { "200": { - "description": "A list of matching CVE records" + "description": "A list of matching CVE records", + "headers": { + "X-Request-ID": { + "description": "Request correlation ID", + "schema": { + "type": "string" + } + } + } }, "400": { "description": "Bad Request" @@ -120,7 +128,15 @@ "operationId": "healthCheck", "responses": { "200": { - "description": "OK" + "description": "OK", + "headers": { + "X-Request-ID": { + "description": "Request correlation ID", + "schema": { + "type": "string" + } + } + } } } } @@ -135,7 +151,15 @@ "operationId": "webSearch", "responses": { "200": { - "description": "A list of matching CVE records" + "description": "A list of matching CVE records", + "headers": { + "X-Request-ID": { + "description": "Request correlation ID", + "schema": { + "type": "string" + } + } + } }, "400": { "description": "Bad Request" diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 9c3424c..6477741 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -107,6 +107,17 @@ describe('OpenAPI contract', () => { expect(openapi.paths['/search'].post.responses['404'].description).toBe('Invalid cpeName value'); }); + test('documents request correlation headers', () => { + expect(openapi.paths['/search'].post.responses['200'].headers['X-Request-ID']).toStrictEqual({ + description: 'Request correlation ID', + schema: { + type: 'string' + } + }); + expect(openapi.paths['/api/health-check'].get.responses['200'].headers['X-Request-ID']).toBeDefined(); + expect(openapi.paths['/webSearch'].post.responses['200'].headers['X-Request-ID']).toBeDefined(); + }); + test('committed OpenAPI artifact matches generated route documentation', async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cve-openapi-')); const generatedPath = path.join(tempDir, 'openapi.json'); diff --git a/config/custom-environment-variables.jsonc b/config/custom-environment-variables.jsonc index 814ed47..2206e3c 100644 --- a/config/custom-environment-variables.jsonc +++ b/config/custom-environment-variables.jsonc @@ -20,6 +20,10 @@ "openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs", "requestDeadlineMs": "SearchRequestDeadlineMs" }, + "requestLogging": { + "successSampleRate": "RequestLogSuccessSampleRate", + "healthChecks": "RequestLogHealthChecks" + }, // constants for testing node-config // these values are only used to test node-config in AppConfig.test.int.ts // DO NOT USE THIS FOR ANYTHING ELSE diff --git a/config/default.jsonc b/config/default.jsonc index da11376..5dc2578 100644 --- a/config/default.jsonc +++ b/config/default.jsonc @@ -29,6 +29,13 @@ "openSearchRequestTimeoutMs": "20000", // Limits total processing time for a /search request. "requestDeadlineMs": "30000" + }, + // Access logging settings. Error responses are always logged. + "requestLogging": { + // Fraction of successful non-health-check requests to log, from 0 through 1. + "successSampleRate": "0.1", + // Set to TRUE to retain successful health-check access logs. + "healthChecks": "FALSE" } } } diff --git a/config/runtime-config.test.js b/config/runtime-config.test.js index a023610..90eba8a 100644 --- a/config/runtime-config.test.js +++ b/config/runtime-config.test.js @@ -102,6 +102,8 @@ describe('runtime container and compose configuration', () => { '"openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs"' ); expect(customEnvironmentVariables).toContain('"requestDeadlineMs": "SearchRequestDeadlineMs"'); + expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"'); + expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"'); }); test.each([['config/default.jsonc'], ['config/devel.jsonc'], ['config/prod.jsonc']])( diff --git a/controllers/searchController.js b/controllers/searchController.js index 0c14203..bf2eb1a 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -322,21 +322,35 @@ async function executeSearch(searchManager, searchOptions, queryStringObjects, p async function searchHandler(req, res, next) { const params = req.searchParams ?? req.query ?? {}; - const metrics = {}; + const metrics = { + requestId: req.requestId + }; const handlerStart = searchPerformance.markTime(); + let searchOptions; + let postFilters; + let responseOptions; try { const searchManager = searchProvider.getAdvancedSearchManager(); - const { searchOptions, queryStringObjects, postFilters, responseOptions } = - searchQueryBuilder.buildSearchRequest(params); + const searchRequest = searchQueryBuilder.buildSearchRequest(params); + searchOptions = searchRequest.searchOptions; + postFilters = searchRequest.postFilters; + responseOptions = searchRequest.responseOptions; const resp = await searchDeadline.withSearchDeadline( - executeSearch(searchManager, searchOptions, queryStringObjects, postFilters, responseOptions, metrics), + executeSearch( + searchManager, + searchOptions, + searchRequest.queryStringObjects, + postFilters, + responseOptions, + metrics + ), searchProvider.getSearchRequestDeadlineMs() ); const pagingOptions = responseOptions ?? searchOptions; - metrics.totalElapsedMs = searchPerformance.elapsedMs(handlerStart); - searchPerformance.logSearchPerformance(metrics, searchOptions, responseOptions, postFilters); + metrics.outcome = 'success'; + metrics.statusCode = 200; //Calculate current page and total pages to append to JSON output const totalValue = resp.data.hits.total.value; @@ -352,26 +366,43 @@ async function searchHandler(req, res, next) { }); } catch (err) { if (searchPostFilterWorkQueue.isQueueFullError(err)) { + metrics.outcome = 'queue_full'; + metrics.statusCode = 429; + metrics.coldSearchQueueState = err.state; if (typeof res.set === 'function') res.set('Retry-After', '5'); return res.status(429).json(error.searchQueueFullError()); } if (searchPostFilterWorkQueue.isQueueTimeoutError(err)) { + metrics.outcome = 'queue_timeout'; + metrics.statusCode = 429; + metrics.coldSearchQueueState = err.state; + metrics.coldSearchQueueWaitElapsedMs = err.waitElapsedMs; if (typeof res.set === 'function') res.set('Retry-After', '5'); return res.status(429).json(error.searchQueueTimeoutError()); } if (searchDeadline.isSearchTimeoutError(err)) { + metrics.outcome = 'search_timeout'; + metrics.statusCode = 504; + metrics.timeoutSource = err?.name === 'TimeoutError' ? 'opensearch' : 'request_deadline'; return res.status(504).json(error.searchTimeoutError()); } - if (err.name === 'ConnectionError') { + if (err?.name === 'ConnectionError') { + metrics.outcome = 'connection_error'; + metrics.statusCode = 400; return res.status(400).json(error.openSearchError()); } + metrics.outcome = 'upstream_error'; + metrics.statusCode = 502; return res.status(502).json(error.upstreamSearchError()); + } finally { + metrics.totalElapsedMs = searchPerformance.elapsedMs(handlerStart); + searchPerformance.logSearchPerformance(metrics, searchOptions, responseOptions, postFilters); } } diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 28a1f93..64f8648 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -773,6 +773,7 @@ describe('Search controller SEARCH handler', () => { await controller.SEARCH( { + requestId: 'queue-full-request', query: { virtualMatchString: 'cpe:2.3:o:microsoft:windows_10_1809', resultsPerPage: '1', @@ -789,6 +790,21 @@ describe('Search controller SEARCH handler', () => { error: 'SEARCH_QUEUE_FULL', message: 'Search service is busy. Please retry later.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'queue-full-request', + outcome: 'queue_full', + statusCode: 429, + strategy: 'cpe-candidate', + cacheStatus: 'miss', + coldSearchQueueState: { + activeCount: 1, + queuedCount: 0, + maxConcurrent: 1, + maxQueueDepth: 0, + maxWaitMs: 10000 + } + }); candidateSearch.resolve(rawResponse(0, [])); await firstSearch; @@ -820,6 +836,7 @@ describe('Search controller SEARCH handler', () => { const secondSearch = controller.SEARCH( { + requestId: 'queue-timeout-request', query: { virtualMatchString: 'cpe:2.3:o:microsoft:windows_10_1809', resultsPerPage: '1', @@ -840,6 +857,15 @@ describe('Search controller SEARCH handler', () => { error: 'SEARCH_QUEUE_TIMEOUT', message: 'Search service is busy. Please retry later.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'queue-timeout-request', + outcome: 'queue_timeout', + statusCode: 429, + strategy: 'cpe-candidate', + cacheStatus: 'miss', + coldSearchQueueWaitElapsedMs: expect.any(Number) + }); candidateSearch.resolve(rawResponse(0, [])); await firstSearch; @@ -874,6 +900,7 @@ describe('Search controller SEARCH handler', () => { await controller.SEARCH( { + requestId: 'request-123', query: { keywordSearch: 'remote code', resultsPerPage: '1' @@ -888,7 +915,11 @@ describe('Search controller SEARCH handler', () => { expect(timingLog).toMatchObject({ event: 'search.performance', route: '/search', + requestId: 'request-123', + outcome: 'success', + statusCode: 200, strategy: 'standard', + cacheStatus: 'not_applicable', optimizedFallback: false, cacheHit: false, inFlightHit: false, @@ -1170,6 +1201,7 @@ describe('Search controller SEARCH handler', () => { await controller.SEARCH( { + requestId: 'connection-error-request', query: {} }, res, @@ -1182,6 +1214,12 @@ describe('Search controller SEARCH handler', () => { message: 'OpenSearch refused connection, please check that OpenSearch is running and accessible then try again.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'connection-error-request', + outcome: 'connection_error', + statusCode: 400 + }); }); test('SEARCH returns a sanitized 504 when OpenSearch times out', async () => { @@ -1192,6 +1230,7 @@ describe('Search controller SEARCH handler', () => { await controller.SEARCH( { + requestId: 'timeout-request', query: {} }, res, @@ -1203,6 +1242,13 @@ describe('Search controller SEARCH handler', () => { error: 'SEARCH_TIMEOUT', message: 'Search request timed out. Please try again later.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'timeout-request', + outcome: 'search_timeout', + statusCode: 504, + timeoutSource: 'opensearch' + }); }); test('SEARCH returns a sanitized 504 when the overall request deadline is exceeded', async () => { @@ -1211,6 +1257,7 @@ describe('Search controller SEARCH handler', () => { const search = controller.SEARCH( { + requestId: 'request-deadline-request', query: {} }, res, @@ -1227,6 +1274,13 @@ describe('Search controller SEARCH handler', () => { error: 'SEARCH_TIMEOUT', message: 'Search request timed out. Please try again later.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'request-deadline-request', + outcome: 'search_timeout', + statusCode: 504, + timeoutSource: 'request_deadline' + }); }); test('SEARCH returns sanitized errors from cve-core', async () => { @@ -1242,6 +1296,7 @@ describe('Search controller SEARCH handler', () => { await controller.SEARCH( { + requestId: 'upstream-error-request', query: {} }, res, @@ -1253,6 +1308,12 @@ describe('Search controller SEARCH handler', () => { error: 'UPSTREAM_SEARCH_ERROR', message: 'Search backend returned an error. Please try again later.' }); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'upstream-error-request', + outcome: 'upstream_error', + statusCode: 502 + }); }); }); diff --git a/routes/index.js b/routes/index.js index 0264af1..9a10b91 100644 --- a/routes/index.js +++ b/routes/index.js @@ -186,6 +186,14 @@ router.post( } #swagger.responses[200] = { description: 'A list of matching CVE records', +headers: { + "X-Request-ID": { + description: "Request correlation ID", + schema: { + type: "string" + } + } +} } #swagger.responses[404] = { description: 'Invalid cpeName value', @@ -237,6 +245,14 @@ router.get( #swagger.description = "Health check for AWS monitoring" #swagger.responses[200] = { description: 'OK', +headers: { + "X-Request-ID": { + description: "Request correlation ID", + schema: { + type: "string" + } + } +} } */ healthCheck @@ -294,6 +310,14 @@ router.post( } #swagger.responses[200] = { description: 'A list of matching CVE records', +headers: { + "X-Request-ID": { + description: "Request correlation ID", + schema: { + type: "string" + } + } +} } #swagger.responses[504] = { description: 'Search exceeded its OpenSearch request deadline', diff --git a/server.js b/server.js index da3cfd8..d76f476 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,7 @@ const http = require('http'); const logger = require('morgan'); const path = require('path'); const router = require('./routes/index'); +const requestLogging = require('./utils/requestLogging'); dotenv.load(); @@ -22,22 +23,22 @@ const swaggerSetupOptions = { .swagger-ui .parameters-col_description input { display: none }` }; -function getRedactedRequestPath(req) { - return req.path || String(req.originalUrl || req.url || '-').split('?')[0]; -} - function createRequestLogger() { if (process.env.NODE_ENV === 'test') { return (_req, _res, next) => next(); } - logger.token('redacted-url', getRedactedRequestPath); - return logger(':method :redacted-url :status :res[content-length] - :response-time ms'); + logger.token('redacted-url', requestLogging.getRedactedRequestPath); + logger.token('request-id', (req) => req.requestId || '-'); + return logger(':method :redacted-url :status :res[content-length] - :response-time ms request-id=:request-id', { + skip: requestLogging.shouldSkipAccessLog + }); } function createApp() { const app = express(); + app.use(requestLogging.assignRequestId); app.use(createRequestLogger()); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); @@ -73,7 +74,7 @@ if (require.main === module) { module.exports = { createApp, createRequestLogger, - getRedactedRequestPath, + getRedactedRequestPath: requestLogging.getRedactedRequestPath, getRuntimeConfig, startServer, swaggerSetupOptions diff --git a/server.test.js b/server.test.js index 6ed4e57..377fb70 100644 --- a/server.test.js +++ b/server.test.js @@ -68,6 +68,18 @@ describe('server application bootstrap', () => { expect(response.status).toBe(200); expect(response.text).toBe(''); expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(response.headers['x-request-id']).toMatch(/^[0-9a-f-]{36}$/); + }); + + test('createApp preserves valid caller request IDs in response headers', async () => { + const response = await request(createApp(), { + path: '/api/health-check', + headers: { + 'X-Request-ID': 'caller-request-123' + } + }); + + expect(response.headers['x-request-id']).toBe('caller-request-123'); }); test('createApp mounts the OpenAPI JSON endpoint', async () => { @@ -187,8 +199,12 @@ describe('server application bootstrap', () => { createRequestLogger(); expect(mockMorgan.token).toHaveBeenCalledWith('redacted-url', getRedactedRequestPath); + expect(mockMorgan.token).toHaveBeenCalledWith('request-id', expect.any(Function)); expect(mockMorgan).toHaveBeenCalledWith( - ':method :redacted-url :status :res[content-length] - :response-time ms' + ':method :redacted-url :status :res[content-length] - :response-time ms request-id=:request-id', + { + skip: expect.any(Function) + } ); }); diff --git a/test/http.js b/test/http.js index af737ec..f437097 100644 --- a/test/http.js +++ b/test/http.js @@ -8,7 +8,7 @@ function createRouterApp(router) { return app; } -function request(app, { method = 'GET', path, body }) { +function request(app, { method = 'GET', path, body, headers = {} }) { return new Promise((resolve, reject) => { const server = app.listen(0, () => { const payload = body ? JSON.stringify(body) : undefined; @@ -19,12 +19,15 @@ function request(app, { method = 'GET', path, body }) { port, path, method, - headers: payload - ? { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload) - } - : {} + headers: { + ...headers, + ...(payload + ? { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload) + } + : {}) + } }, (res) => { let text = ''; diff --git a/utils/requestLogging.js b/utils/requestLogging.js new file mode 100644 index 0000000..a363e83 --- /dev/null +++ b/utils/requestLogging.js @@ -0,0 +1,98 @@ +const { randomUUID } = require('crypto'); +const { AppConfig } = require('cve-core'); + +const DEFAULT_SUCCESS_SAMPLE_RATE = 0.1; +const DEFAULT_LOG_HEALTH_CHECKS = false; +const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/; + +let requestLoggingConfig; + +function normalizeBoolean(value, defaultValue = false) { + if (value === undefined || value === null || value === '') return defaultValue; + if (typeof value === 'boolean') return value; + if (typeof value === 'number') return value !== 0; + + const normalized = String(value).trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false; + + return defaultValue; +} + +function readSuccessSampleRate() { + if (!AppConfig.has('requestLogging.successSampleRate')) return DEFAULT_SUCCESS_SAMPLE_RATE; + + const value = Number(AppConfig.get('requestLogging.successSampleRate')); + if (Number.isFinite(value) && value >= 0 && value <= 1) return value; + + return DEFAULT_SUCCESS_SAMPLE_RATE; +} + +function readLogHealthChecks() { + if (!AppConfig.has('requestLogging.healthChecks')) return DEFAULT_LOG_HEALTH_CHECKS; + + return normalizeBoolean(AppConfig.get('requestLogging.healthChecks'), DEFAULT_LOG_HEALTH_CHECKS); +} + +function getRequestLoggingConfig() { + if (!requestLoggingConfig) { + requestLoggingConfig = Object.freeze({ + successSampleRate: readSuccessSampleRate(), + healthChecks: readLogHealthChecks() + }); + } + + return requestLoggingConfig; +} + +function getSuccessSampleRate() { + return getRequestLoggingConfig().successSampleRate; +} + +function shouldLogHealthChecks() { + return getRequestLoggingConfig().healthChecks; +} + +function getRedactedRequestPath(req) { + return req.path || String(req.originalUrl || req.url || '-').split('?')[0]; +} + +function resolveRequestId(req, generateId = randomUUID) { + const headerValue = req.get?.('x-request-id') ?? req.headers?.['x-request-id']; + const requestId = Array.isArray(headerValue) ? headerValue[0] : headerValue; + const normalized = typeof requestId === 'string' ? requestId.trim() : ''; + + return REQUEST_ID_PATTERN.test(normalized) ? normalized : generateId(); +} + +function assignRequestId(req, res, next) { + req.requestId = resolveRequestId(req); + res.setHeader('X-Request-ID', req.requestId); + next(); +} + +function shouldSkipAccessLog(req, res, random = Math.random) { + if (res.statusCode >= 400) return false; + + if (getRedactedRequestPath(req) === '/api/health-check' && !shouldLogHealthChecks()) { + return true; + } + + return random() >= getSuccessSampleRate(); +} + +function resetForTests() { + requestLoggingConfig = undefined; +} + +module.exports = { + assignRequestId, + getRedactedRequestPath, + getRequestLoggingConfig, + getSuccessSampleRate, + normalizeBoolean, + resetForTests, + resolveRequestId, + shouldLogHealthChecks, + shouldSkipAccessLog +}; diff --git a/utils/requestLogging.test.js b/utils/requestLogging.test.js new file mode 100644 index 0000000..9d90656 --- /dev/null +++ b/utils/requestLogging.test.js @@ -0,0 +1,150 @@ +const mockAppConfigHas = jest.fn(); +const mockAppConfigGet = jest.fn(); + +jest.mock('cve-core', () => ({ + AppConfig: { + get: mockAppConfigGet, + has: mockAppConfigHas + } +})); + +const requestLogging = require('./requestLogging'); + +describe('requestLogging', () => { + beforeEach(() => { + requestLogging.resetForTests(); + mockAppConfigHas.mockReset(); + mockAppConfigGet.mockReset(); + mockAppConfigHas.mockReturnValue(false); + }); + + test('resolveRequestId preserves safe caller IDs', () => { + expect( + requestLogging.resolveRequestId( + { + headers: { + 'x-request-id': 'caller-request_123' + } + }, + () => 'generated-id' + ) + ).toBe('caller-request_123'); + }); + + test.each(['contains spaces', 'contains\nnewline', '', 'a'.repeat(129)])( + 'resolveRequestId replaces invalid caller ID %p', + (requestId) => { + expect( + requestLogging.resolveRequestId( + { + headers: { + 'x-request-id': requestId + } + }, + () => 'generated-id' + ) + ).toBe('generated-id'); + } + ); + + test('assignRequestId attaches a generated ID to the request and response', () => { + const req = { + headers: {} + }; + const res = { + setHeader: jest.fn() + }; + const next = jest.fn(); + + requestLogging.assignRequestId(req, res, next); + + expect(req.requestId).toMatch(/^[0-9a-f-]{36}$/); + expect(res.setHeader).toHaveBeenCalledWith('X-Request-ID', req.requestId); + expect(next).toHaveBeenCalledTimes(1); + }); + + test('shouldSkipAccessLog suppresses successful health checks by default', () => { + expect( + requestLogging.shouldSkipAccessLog( + { + path: '/api/health-check' + }, + { + statusCode: 200 + }, + () => 0 + ) + ).toBe(true); + }); + + test('shouldSkipAccessLog retains failed health checks', () => { + expect( + requestLogging.shouldSkipAccessLog( + { + path: '/api/health-check' + }, + { + statusCode: 500 + }, + () => 1 + ) + ).toBe(false); + }); + + test('shouldSkipAccessLog samples successful requests at the configured rate', () => { + mockAppConfigHas.mockImplementation((path) => path === 'requestLogging.successSampleRate'); + mockAppConfigGet.mockReturnValue('0.25'); + const req = { + path: '/search' + }; + const res = { + statusCode: 200 + }; + + expect(requestLogging.shouldSkipAccessLog(req, res, () => 0.24)).toBe(false); + expect(requestLogging.shouldSkipAccessLog(req, res, () => 0.25)).toBe(true); + }); + + test('shouldSkipAccessLog retains all client and server errors', () => { + const req = { + path: '/search' + }; + + expect(requestLogging.shouldSkipAccessLog(req, { statusCode: 400 }, () => 1)).toBe(false); + expect(requestLogging.shouldSkipAccessLog(req, { statusCode: 500 }, () => 1)).toBe(false); + }); + + test('request logging settings fall back when config values are invalid', () => { + mockAppConfigHas.mockReturnValue(true); + mockAppConfigGet.mockReturnValue('invalid'); + + expect(requestLogging.getSuccessSampleRate()).toBe(0.1); + expect(requestLogging.shouldLogHealthChecks()).toBe(false); + }); + + test('successful health checks can be enabled through config', () => { + mockAppConfigHas.mockImplementation((path) => path === 'requestLogging.healthChecks'); + mockAppConfigGet.mockReturnValue('TRUE'); + + expect( + requestLogging.shouldSkipAccessLog( + { + path: '/api/health-check' + }, + { + statusCode: 200 + }, + () => 0 + ) + ).toBe(false); + }); + + test('request logging config is resolved once per process', () => { + const first = requestLogging.getRequestLoggingConfig(); + const second = requestLogging.getRequestLoggingConfig(); + + expect(first).toBe(second); + expect(Object.isFrozen(first)).toBe(true); + expect(mockAppConfigHas).toHaveBeenCalledTimes(2); + }); +}); diff --git a/utils/searchPerformance.js b/utils/searchPerformance.js index b5ed5d4..26ebd22 100644 --- a/utils/searchPerformance.js +++ b/utils/searchPerformance.js @@ -12,22 +12,43 @@ function roundMs(value) { return value === undefined ? undefined : Math.round(value * 100) / 100; } +function getCacheStatus(metrics, postFilters) { + if (!Object.keys(postFilters ?? {}).length) return 'not_applicable'; + if (metrics.cacheHit === true) return 'hit'; + if (metrics.inFlightHit === true) return 'in_flight'; + + return 'miss'; +} + function logSearchPerformance(metrics, searchOptions, responseOptions, postFilters) { - if (!searchProvider.shouldLogSearchPerformance()) return; + const outcome = metrics.outcome ?? 'success'; + const includeDetailedTimings = searchProvider.shouldLogSearchPerformance(); + if (outcome === 'success' && !includeDetailedTimings) return; - console.log( - JSON.stringify({ - event: 'search.performance', - route: '/search', - strategy: metrics.strategy, + const event = { + event: 'search.performance', + route: '/search', + requestId: metrics.requestId, + outcome, + statusCode: metrics.statusCode, + timeoutSource: metrics.timeoutSource, + strategy: metrics.strategy, + cacheStatus: getCacheStatus(metrics, postFilters), + coldSearchQueueWaitElapsedMs: roundMs(metrics.coldSearchQueueWaitElapsedMs), + coldSearchQueueState: metrics.coldSearchQueueState, + totalElapsedMs: roundMs(metrics.totalElapsedMs) + }; + + if (includeDetailedTimings) { + Object.assign(event, { optimizedFallback: metrics.optimizedFallback === true, cacheHit: metrics.cacheHit === true, inFlightHit: metrics.inFlightHit === true, pageFetchInFlightHit: metrics.pageFetchInFlightHit === true, postFilters: Object.keys(postFilters ?? {}), - requestedFrom: responseOptions?.from ?? searchOptions.from, - requestedSize: responseOptions?.resultsPerPage ?? searchOptions.resultsPerPage, - openSearchSize: searchOptions.resultsPerPage, + requestedFrom: responseOptions?.from ?? searchOptions?.from, + requestedSize: responseOptions?.resultsPerPage ?? searchOptions?.resultsPerPage, + openSearchSize: searchOptions?.resultsPerPage, candidateCount: metrics.candidateCount, filteredCount: metrics.filteredCount, returnedCount: metrics.returnedCount, @@ -35,18 +56,18 @@ function logSearchPerformance(metrics, searchOptions, responseOptions, postFilte pageFetchTookMs: metrics.pageFetchTookMs, openSearchElapsedMs: roundMs(metrics.openSearchElapsedMs), postFilterElapsedMs: roundMs(metrics.postFilterElapsedMs), - coldSearchQueueWaitElapsedMs: roundMs(metrics.coldSearchQueueWaitElapsedMs), - coldSearchQueueState: metrics.coldSearchQueueState, inFlightWaitElapsedMs: roundMs(metrics.inFlightWaitElapsedMs), pageFetchElapsedMs: roundMs(metrics.pageFetchElapsedMs), - pageFetchWaitElapsedMs: roundMs(metrics.pageFetchWaitElapsedMs), - totalElapsedMs: roundMs(metrics.totalElapsedMs) - }) - ); + pageFetchWaitElapsedMs: roundMs(metrics.pageFetchWaitElapsedMs) + }); + } + + console.log(JSON.stringify(event)); } module.exports = { elapsedMs, + getCacheStatus, logSearchPerformance, markTime, roundMs diff --git a/utils/searchPerformance.test.js b/utils/searchPerformance.test.js index 6930be5..b4f1cfa 100644 --- a/utils/searchPerformance.test.js +++ b/utils/searchPerformance.test.js @@ -21,6 +21,9 @@ describe('searchPerformance', () => { searchPerformance.logSearchPerformance( { + requestId: 'request-123', + outcome: 'success', + statusCode: 200, strategy: 'cpe-candidate', cacheHit: true, inFlightHit: true, @@ -60,7 +63,11 @@ describe('searchPerformance', () => { expect(JSON.parse(console.log.mock.calls[0][0])).toStrictEqual({ event: 'search.performance', route: '/search', + requestId: 'request-123', + outcome: 'success', + statusCode: 200, strategy: 'cpe-candidate', + cacheStatus: 'hit', optimizedFallback: false, cacheHit: true, inFlightHit: true, @@ -92,8 +99,63 @@ describe('searchPerformance', () => { test('logSearchPerformance does not emit logs when disabled', () => { mockShouldLogSearchPerformance.mockReturnValue(false); - searchPerformance.logSearchPerformance({}, { resultsPerPage: 1, from: 0 }, undefined, {}); + searchPerformance.logSearchPerformance( + { + outcome: 'success' + }, + { resultsPerPage: 1, from: 0 }, + undefined, + {} + ); expect(console.log).not.toHaveBeenCalled(); }); + + test('logSearchPerformance always emits sanitized failure outcomes', () => { + mockShouldLogSearchPerformance.mockReturnValue(false); + + searchPerformance.logSearchPerformance( + { + requestId: 'request-456', + outcome: 'queue_timeout', + statusCode: 429, + strategy: 'cpe-candidate', + coldSearchQueueWaitElapsedMs: 10000.123, + coldSearchQueueState: { + activeCount: 10, + queuedCount: 499, + maxConcurrent: 10, + maxQueueDepth: 500, + maxWaitMs: 10000 + }, + totalElapsedMs: 10001.456 + }, + undefined, + undefined, + { + virtualMatchString: 'sensitive-value' + } + ); + + expect(console.log).toHaveBeenCalledTimes(1); + expect(JSON.parse(console.log.mock.calls[0][0])).toStrictEqual({ + event: 'search.performance', + route: '/search', + requestId: 'request-456', + outcome: 'queue_timeout', + statusCode: 429, + strategy: 'cpe-candidate', + cacheStatus: 'miss', + coldSearchQueueWaitElapsedMs: 10000.12, + coldSearchQueueState: { + activeCount: 10, + queuedCount: 499, + maxConcurrent: 10, + maxQueueDepth: 500, + maxWaitMs: 10000 + }, + totalElapsedMs: 10001.46 + }); + expect(console.log.mock.calls[0][0]).not.toContain('sensitive-value'); + }); }); diff --git a/utils/searchPostFilterWorkQueue.js b/utils/searchPostFilterWorkQueue.js index 27c2dd3..b89ae86 100644 --- a/utils/searchPostFilterWorkQueue.js +++ b/utils/searchPostFilterWorkQueue.js @@ -13,11 +13,12 @@ class SearchPostFilterQueueFullError extends Error { } class SearchPostFilterQueueTimeoutError extends Error { - constructor(state) { + constructor(state, waitElapsedMs) { super('CPE post-filter cold search queue wait timed out.'); this.name = 'SearchPostFilterQueueTimeoutError'; this.code = 'SEARCH_QUEUE_TIMEOUT'; this.state = state; + this.waitElapsedMs = waitElapsedMs; } } @@ -83,6 +84,7 @@ function run(task) { task, resolve, reject, + queuedAt: process.hrtime.bigint(), timeoutId: undefined }; @@ -92,7 +94,8 @@ function run(task) { if (queueIndex === -1) return; queue.splice(queueIndex, 1); - reject(new SearchPostFilterQueueTimeoutError(getState())); + const waitElapsedMs = Number(process.hrtime.bigint() - item.queuedAt) / 1e6; + reject(new SearchPostFilterQueueTimeoutError(getState(), waitElapsedMs)); drain(); }, getMaxWaitMs()); item.timeoutId.unref?.(); diff --git a/utils/searchPostFilterWorkQueue.test.js b/utils/searchPostFilterWorkQueue.test.js index bea5751..b8406f3 100644 --- a/utils/searchPostFilterWorkQueue.test.js +++ b/utils/searchPostFilterWorkQueue.test.js @@ -134,6 +134,7 @@ describe('searchPostFilterWorkQueue', () => { const secondRun = searchPostFilterWorkQueue.run(secondTask); const timeoutExpectation = expect(secondRun).rejects.toMatchObject({ code: 'SEARCH_QUEUE_TIMEOUT', + waitElapsedMs: expect.any(Number), state: { activeCount: 1, queuedCount: 0, From 0459724820c5ea2cf754d885138395f2a4fedeff Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 27 Jul 2026 14:20:57 -0400 Subject: [PATCH 05/20] Gate deprecated web search endpoint --- .env.example | 1 + README.md | 5 ++- api-docs/openapi.json | 10 ++++-- api-docs/openapi.test.js | 3 ++ config/custom-environment-variables.jsonc | 3 ++ config/default.jsonc | 5 +++ config/runtime-config.test.js | 1 + routes/index.js | 18 ++++++++-- routes/index.test.js | 23 ++++++++++++ utils/errors.js | 4 +++ utils/errors.test.js | 8 +++++ utils/featureFlags.js | 25 +++++++++++++ utils/featureFlags.test.js | 44 +++++++++++++++++++++++ 13 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 utils/featureFlags.js create mode 100644 utils/featureFlags.test.js diff --git a/.env.example b/.env.example index 8371eb3..2db0684 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,5 @@ SearchOpenSearchRequestTimeoutMs=20000 SearchRequestDeadlineMs=30000 RequestLogSuccessSampleRate=0.1 RequestLogHealthChecks=false +WebSearchEnabled=true NPM_CA_CERT= diff --git a/README.md b/README.md index aa43f12..ca4dd18 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Rename `.env.example` to `.env` and replace or check the following values. - `SearchRequestDeadlineMs` - optional overall `/search` processing deadline, default `30000` - `RequestLogSuccessSampleRate` - optional fraction of successful non-health-check access logs to retain, default `0.1` - `RequestLogHealthChecks` - optional flag to retain successful health-check access logs, default `false` +- `WebSearchEnabled` - optional deprecated `/webSearch` feature flag, default `true` - `NPM_CA_CERT` - optional absolute host path to a corporate or self-signed CA certificate bundle used only during local Docker Compose builds ```bash @@ -158,7 +159,9 @@ Version ranges use overlap semantics. A stored wildcard-version CPE Match Criter Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. That would intentionally diverge from NVD behavior and should be handled as a policy decision before implementation. -`POST /webSearch` requires a non-empty JSON `query` string. It also accepts optional `sort`, `from`, and `size` fields: +`POST /webSearch` is deprecated in favor of `POST /search`. It remains enabled by default during consumer migration and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch. + +While enabled, `POST /webSearch` requires a non-empty JSON `query` string. It also accepts optional `sort`, `from`, and `size` fields: ```json { diff --git a/api-docs/openapi.json b/api-docs/openapi.json index dbcf6e7..b65a34b 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -146,8 +146,8 @@ "tags": [ "CVE Record" ], - "summary": "Retrieves all matching CVE records after applying the JSON request body", - "description": "

Expected Behavior

Retrieves all matching CVE records after applying the JSON request body

", + "summary": "Deprecated raw query search", + "description": "

Expected Behavior

Retrieves all matching CVE records after applying the JSON request body. This endpoint is deprecated and can be disabled by the service operator. Use POST /search instead.

", "operationId": "webSearch", "responses": { "200": { @@ -164,6 +164,9 @@ "400": { "description": "Bad Request" }, + "410": { + "description": "The deprecated endpoint is disabled; use POST /search" + }, "504": { "description": "Search exceeded its OpenSearch request deadline" } @@ -216,7 +219,8 @@ } } } - } + }, + "deprecated": true } } }, diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 6477741..3716db9 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -85,6 +85,8 @@ describe('OpenAPI contract', () => { const operation = openapi.paths['/webSearch'].post; const schema = operation.requestBody.content['application/json'].schema; + expect(operation.deprecated).toBe(true); + expect(operation.description).toContain('Use POST /search instead'); expect(operation.parameters || []).toStrictEqual([]); expect(operation.requestBody.required).toBe(true); expect(schema.required).toStrictEqual(['query']); @@ -93,6 +95,7 @@ describe('OpenAPI contract', () => { expect(schema.properties.sort.additionalProperties).toBe(false); expect(schema.properties.sort.properties.property.enum).toStrictEqual(['cveId', 'relevancy']); expect(schema.properties.size.maximum).toBe(constants.MAX_RESULTS_LIMIT); + expect(operation.responses['410'].description).toContain('use POST /search'); }); test('documents cpeName as a CPE Match Criteria filter with required components', () => { diff --git a/config/custom-environment-variables.jsonc b/config/custom-environment-variables.jsonc index 2206e3c..1f10185 100644 --- a/config/custom-environment-variables.jsonc +++ b/config/custom-environment-variables.jsonc @@ -24,6 +24,9 @@ "successSampleRate": "RequestLogSuccessSampleRate", "healthChecks": "RequestLogHealthChecks" }, + "features": { + "webSearchEnabled": "WebSearchEnabled" + }, // constants for testing node-config // these values are only used to test node-config in AppConfig.test.int.ts // DO NOT USE THIS FOR ANYTHING ELSE diff --git a/config/default.jsonc b/config/default.jsonc index 5dc2578..e381f4c 100644 --- a/config/default.jsonc +++ b/config/default.jsonc @@ -36,6 +36,11 @@ "successSampleRate": "0.1", // Set to TRUE to retain successful health-check access logs. "healthChecks": "FALSE" + }, + // Feature flags used for controlled endpoint migrations. + "features": { + // Keep enabled until existing consumers have migrated to POST /search. + "webSearchEnabled": "TRUE" } } } diff --git a/config/runtime-config.test.js b/config/runtime-config.test.js index 90eba8a..9583c4d 100644 --- a/config/runtime-config.test.js +++ b/config/runtime-config.test.js @@ -104,6 +104,7 @@ describe('runtime container and compose configuration', () => { expect(customEnvironmentVariables).toContain('"requestDeadlineMs": "SearchRequestDeadlineMs"'); expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"'); expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"'); + expect(customEnvironmentVariables).toContain('"webSearchEnabled": "WebSearchEnabled"'); }); test.each([['config/default.jsonc'], ['config/devel.jsonc'], ['config/prod.jsonc']])( diff --git a/routes/index.js b/routes/index.js index 9a10b91..2e0b2e4 100644 --- a/routes/index.js +++ b/routes/index.js @@ -4,6 +4,7 @@ const errors = require('../utils/errors'); const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); const constants = require('../utils/constants'); +const featureFlags = require('../utils/featureFlags'); const error = new errors.errors(); const SEARCH_BODY_FIELDS = new Set(constants.SEARCH_QUERY_PARAMS); @@ -92,6 +93,14 @@ function rejectSearchQueryParams(req, res, next) { next(); } +function requireWebSearchEnabled(req, res, next) { + if (!featureFlags.isWebSearchEnabled()) { + return res.status(410).json(error.webSearchDisabledError()); + } + + next(); +} + function normalizeSearchParams(req, res, next) { const searchParams = { ...(req.body ?? {}) }; @@ -263,10 +272,11 @@ router.post( /* #swagger.tags = ['CVE Record'] #swagger.operationId = 'webSearch' -#swagger.summary = "Retrieves all matching CVE records after applying the JSON request body" +#swagger.deprecated = true +#swagger.summary = "Deprecated raw query search" #swagger.description = "

Expected Behavior

-

Retrieves all matching CVE records after applying the JSON request body

+

Retrieves all matching CVE records after applying the JSON request body. This endpoint is deprecated and can be disabled by the service operator. Use POST /search instead.

#swagger.requestBody = { required: true, content: { @@ -322,7 +332,11 @@ headers: { #swagger.responses[504] = { description: 'Search exceeded its OpenSearch request deadline', } +#swagger.responses[410] = { +description: 'The deprecated endpoint is disabled; use POST /search', +} */ + requireWebSearchEnabled, body().custom(hasOnlyWebSearchBodyFields).withMessage(error.webSearchUnsupportedFieldError()), body('query') .exists({ checkFalsy: true }) diff --git a/routes/index.test.js b/routes/index.test.js index 5102fc9..0f6a072 100644 --- a/routes/index.test.js +++ b/routes/index.test.js @@ -19,8 +19,13 @@ jest.mock('../controllers/searchController', () => ({ ) })); +jest.mock('../utils/featureFlags', () => ({ + isWebSearchEnabled: jest.fn(() => true) +})); + const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); +const featureFlags = require('../utils/featureFlags'); const router = require('./index'); describe('routes/index router', () => { @@ -29,6 +34,7 @@ describe('routes/index router', () => { beforeEach(() => { app = createRouterApp(router); jest.clearAllMocks(); + featureFlags.isWebSearchEnabled.mockReturnValue(true); }); test('GET /api/health-check returns 200', async () => { @@ -239,6 +245,23 @@ describe('routes/index router', () => { }); }); + test('POST /webSearch returns 410 before validation and controller work when disabled', async () => { + featureFlags.isWebSearchEnabled.mockReturnValue(false); + + const response = await request(app, { + method: 'POST', + path: '/webSearch', + body: {} + }); + + expect(response.status).toBe(410); + expect(response.body).toStrictEqual({ + error: 'WEB_SEARCH_DISABLED', + message: 'The /webSearch endpoint is disabled. Use POST /search instead.' + }); + expect(controller.WEB_SEARCH).not.toHaveBeenCalled(); + }); + test('POST /webSearch trims query and normalizes integer body fields before the controller runs', async () => { const response = await request(app, { method: 'POST', diff --git a/utils/errors.js b/utils/errors.js index 3640826..edd76be 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -229,6 +229,10 @@ class errors { return makeError('WEB_SEARCH_UNSUPPORTED_FIELD', 'The /webSearch request body contains unsupported fields.'); } + webSearchDisabledError() { + return makeError('WEB_SEARCH_DISABLED', 'The /webSearch endpoint is disabled. Use POST /search instead.'); + } + openSearchError() { return makeError( 'OPEN_SEARCH_ERROR', diff --git a/utils/errors.test.js b/utils/errors.test.js index 23f3b15..c73b512 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -346,6 +346,14 @@ describe('error response helpers', () => { message: 'The /webSearch request body contains unsupported fields.' } ], + [ + 'webSearchDisabledError', + [], + { + error: 'WEB_SEARCH_DISABLED', + message: 'The /webSearch endpoint is disabled. Use POST /search instead.' + } + ], [ 'openSearchError', [], diff --git a/utils/featureFlags.js b/utils/featureFlags.js new file mode 100644 index 0000000..61ff67f --- /dev/null +++ b/utils/featureFlags.js @@ -0,0 +1,25 @@ +const { AppConfig } = require('cve-core'); + +const DEFAULT_WEB_SEARCH_ENABLED = true; + +function normalizeBoolean(value, defaultValue) { + if (value === undefined || value === null || value === '') return defaultValue; + if (typeof value === 'boolean') return value; + if (typeof value === 'number') return value !== 0; + + const normalized = String(value).trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false; + + return defaultValue; +} + +function isWebSearchEnabled() { + if (!AppConfig.has('features.webSearchEnabled')) return DEFAULT_WEB_SEARCH_ENABLED; + + return normalizeBoolean(AppConfig.get('features.webSearchEnabled'), false); +} + +module.exports = { + isWebSearchEnabled +}; diff --git a/utils/featureFlags.test.js b/utils/featureFlags.test.js new file mode 100644 index 0000000..3d47d75 --- /dev/null +++ b/utils/featureFlags.test.js @@ -0,0 +1,44 @@ +const mockAppConfigHas = jest.fn(); +const mockAppConfigGet = jest.fn(); + +jest.mock('cve-core', () => ({ + AppConfig: { + get: mockAppConfigGet, + has: mockAppConfigHas + } +})); + +const featureFlags = require('./featureFlags'); + +describe('featureFlags', () => { + beforeEach(() => { + mockAppConfigHas.mockReset(); + mockAppConfigGet.mockReset(); + mockAppConfigHas.mockReturnValue(false); + }); + + test('keeps webSearch enabled when no override is configured', () => { + expect(featureFlags.isWebSearchEnabled()).toBe(true); + }); + + test.each([ + ['TRUE', true], + ['false', false], + ['1', true], + ['0', false], + [true, true], + [false, false] + ])('normalizes a configured webSearch value of %p', (configuredValue, expected) => { + mockAppConfigHas.mockReturnValue(true); + mockAppConfigGet.mockReturnValue(configuredValue); + + expect(featureFlags.isWebSearchEnabled()).toBe(expected); + }); + + test('fails closed when an explicitly configured webSearch value is invalid', () => { + mockAppConfigHas.mockReturnValue(true); + mockAppConfigGet.mockReturnValue('invalid'); + + expect(featureFlags.isWebSearchEnabled()).toBe(false); + }); +}); From 6629d98bed26760d187616b9dd3f6c3b9a078350 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 27 Jul 2026 14:27:24 -0400 Subject: [PATCH 06/20] Add fallback search rate limiting --- .env.example | 3 + README.md | 11 ++ api-docs/openapi.json | 22 +++- api-docs/openapi.test.js | 1 + config/custom-environment-variables.jsonc | 7 ++ config/default.jsonc | 10 ++ config/runtime-config.test.js | 3 + package-lock.json | 46 ++++++++ package.json | 1 + routes/index.js | 25 ++++- routes/index.test.js | 31 ++++++ utils/errors.js | 4 + utils/errors.test.js | 8 ++ utils/searchRateLimiter.js | 100 +++++++++++++++++ utils/searchRateLimiter.test.js | 127 ++++++++++++++++++++++ 15 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 utils/searchRateLimiter.js create mode 100644 utils/searchRateLimiter.test.js diff --git a/.env.example b/.env.example index 2db0684..0b81c9b 100644 --- a/.env.example +++ b/.env.example @@ -13,4 +13,7 @@ SearchRequestDeadlineMs=30000 RequestLogSuccessSampleRate=0.1 RequestLogHealthChecks=false WebSearchEnabled=true +SearchRateLimitEnabled=false +SearchRateLimitWindowMs=60000 +SearchRateLimitMaxRequests=0 NPM_CA_CERT= diff --git a/README.md b/README.md index ca4dd18..bb6300a 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ Rename `.env.example` to `.env` and replace or check the following values. - `RequestLogSuccessSampleRate` - optional fraction of successful non-health-check access logs to retain, default `0.1` - `RequestLogHealthChecks` - optional flag to retain successful health-check access logs, default `false` - `WebSearchEnabled` - optional deprecated `/webSearch` feature flag, default `true` +- `SearchRateLimitEnabled` - optional per-process `/search` rate-limit flag, default `false` +- `SearchRateLimitWindowMs` - optional rate-limit window in milliseconds, default `60000` +- `SearchRateLimitMaxRequests` - required positive per-process request limit when rate limiting is enabled - `NPM_CA_CERT` - optional absolute host path to a corporate or self-signed CA certificate bundle used only during local Docker Compose builds ```bash @@ -217,6 +220,14 @@ The API always emits a sanitized JSON `search.performance` event for `/search` q Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. Successful requests then emit the same structured event with additional redacted timing details, including candidate counts, filtered counts, returned count, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. +### Application Rate Limiting + +Application rate limiting is disabled by default and applies only to `POST /search`. It does not apply to `/webSearch`, health checks, Swagger, OpenAPI, or static assets. + +The fallback limiter uses one in-memory counter per Node.js process. Set `SearchRateLimitEnabled=true`, provide a positive `SearchRateLimitMaxRequests`, and optionally change `SearchRateLimitWindowMs` to enable it. Invalid enabled configuration prevents the application from starting. Rejected requests return HTTP 429 with `SEARCH_RATE_LIMITED`, `Retry-After`, `RateLimit`, and `RateLimit-Policy` headers, and emit a sanitized `search.performance` event with the `rate_limited` outcome. + +This is a process-wide safety limit, not a precise distributed quota. Each ECS task has an independent counter, so total permitted traffic scales with the number of tasks and load-balancer routing. It deliberately does not use caller IP addresses or forwarded headers until the ALB proxy chain and Express `trust proxy` setting are confirmed. AWS WAF or another infrastructure-level distributed limit remains the preferred public-facing control. + ### CPE Post-Filter Cache Searches that use `cpeName`, `virtualMatchString`, or `virtualMatchString` with version range parameters use a semantic CPE post-filter. The API first asks OpenSearch for up to 10,000 lightweight candidate records, filters those candidates in Node.js, then fetches the requested page of full CVE records by CVE ID. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index b65a34b..eea8e4d 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -42,7 +42,27 @@ "description": "Invalid cpeName value" }, "429": { - "description": "Cold CPE search queue is full or exceeded its maximum wait" + "description": "Application rate limit exceeded, or the cold CPE search queue is full or exceeded its maximum wait", + "headers": { + "Retry-After": { + "description": "Seconds until a rate-limited request can be retried", + "schema": { + "type": "integer" + } + }, + "RateLimit": { + "description": "Current application rate-limit state when application limiting is enabled", + "schema": { + "type": "string" + } + }, + "RateLimit-Policy": { + "description": "Application rate-limit policy when application limiting is enabled", + "schema": { + "type": "string" + } + } + } }, "504": { "description": "Search exceeded its OpenSearch or overall processing deadline" diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 3716db9..1083eb7 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -108,6 +108,7 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('virtualMatchString'); expect(cpeName.example).toBe('cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*'); expect(openapi.paths['/search'].post.responses['404'].description).toBe('Invalid cpeName value'); + expect(openapi.paths['/search'].post.responses['429'].description).toContain('rate limit'); }); test('documents request correlation headers', () => { diff --git a/config/custom-environment-variables.jsonc b/config/custom-environment-variables.jsonc index 1f10185..a194540 100644 --- a/config/custom-environment-variables.jsonc +++ b/config/custom-environment-variables.jsonc @@ -27,6 +27,13 @@ "features": { "webSearchEnabled": "WebSearchEnabled" }, + "rateLimiting": { + "search": { + "enabled": "SearchRateLimitEnabled", + "windowMs": "SearchRateLimitWindowMs", + "maxRequests": "SearchRateLimitMaxRequests" + } + }, // constants for testing node-config // these values are only used to test node-config in AppConfig.test.int.ts // DO NOT USE THIS FOR ANYTHING ELSE diff --git a/config/default.jsonc b/config/default.jsonc index e381f4c..124a926 100644 --- a/config/default.jsonc +++ b/config/default.jsonc @@ -41,6 +41,16 @@ "features": { // Keep enabled until existing consumers have migrated to POST /search. "webSearchEnabled": "TRUE" + }, + // Application-level fallback protection. Infrastructure-level enforcement remains preferred. + "rateLimiting": { + "search": { + // Disabled until a per-task threshold is approved. + "enabled": "FALSE", + "windowMs": "60000", + // A positive value is required when enabled. + "maxRequests": "0" + } } } } diff --git a/config/runtime-config.test.js b/config/runtime-config.test.js index 9583c4d..67946b3 100644 --- a/config/runtime-config.test.js +++ b/config/runtime-config.test.js @@ -105,6 +105,9 @@ describe('runtime container and compose configuration', () => { expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"'); expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"'); expect(customEnvironmentVariables).toContain('"webSearchEnabled": "WebSearchEnabled"'); + expect(customEnvironmentVariables).toContain('"enabled": "SearchRateLimitEnabled"'); + expect(customEnvironmentVariables).toContain('"windowMs": "SearchRateLimitWindowMs"'); + expect(customEnvironmentVariables).toContain('"maxRequests": "SearchRateLimitMaxRequests"'); }); test.each([['config/default.jsonc'], ['config/devel.jsonc'], ['config/prod.jsonc']])( diff --git a/package-lock.json b/package-lock.json index 258e05f..ca4464e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "debug": "~3.2.7", "dotenv": "^6.1.0", "express": "^4.22.2", + "express-rate-limit": "^8.6.1", "express-validator": "^7.2.1", "http-errors": "~1.8.1", "morgan": "~1.10.0", @@ -2936,6 +2937,42 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-rate-limit": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz", + "integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-rate-limit/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/express-validator": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.1.tgz", @@ -3593,6 +3630,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.3.1.tgz", + "integrity": "sha512-1e9d3kb97NHJTIJDZW9rKqW2h6+dFa50Dy0fpPSMQp2ADje5gvKsXmdiK6dwY5t76TaTt5+P5N1Y/LoToIxP6g==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index 5b656fe..dd7d2f2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "debug": "~3.2.7", "dotenv": "^6.1.0", "express": "^4.22.2", + "express-rate-limit": "^8.6.1", "express-validator": "^7.2.1", "http-errors": "~1.8.1", "morgan": "~1.10.0", diff --git a/routes/index.js b/routes/index.js index 2e0b2e4..7604253 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,8 +5,10 @@ const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); const constants = require('../utils/constants'); const featureFlags = require('../utils/featureFlags'); +const searchRateLimiter = require('../utils/searchRateLimiter'); const error = new errors.errors(); +const limitSearchRequests = searchRateLimiter.createSearchRateLimiter(); const SEARCH_BODY_FIELDS = new Set(constants.SEARCH_QUERY_PARAMS); const SEARCH_STRING_BODY_FIELDS = constants.SEARCH_QUERY_PARAMS.filter( (field) => @@ -208,12 +210,33 @@ headers: { description: 'Invalid cpeName value', } #swagger.responses[429] = { -description: 'Cold CPE search queue is full or exceeded its maximum wait', +description: 'Application rate limit exceeded, or the cold CPE search queue is full or exceeded its maximum wait', +headers: { + "Retry-After": { + description: "Seconds until a rate-limited request can be retried", + schema: { + type: "integer" + } + }, + "RateLimit": { + description: "Current application rate-limit state when application limiting is enabled", + schema: { + type: "string" + } + }, + "RateLimit-Policy": { + description: "Application rate-limit policy when application limiting is enabled", + schema: { + type: "string" + } + } +} } #swagger.responses[504] = { description: 'Search exceeded its OpenSearch or overall processing deadline', } */ + limitSearchRequests, rejectSearchQueryParams, body().custom(isSearchBodyObject).withMessage(error.searchBodyError()), body().custom(hasOnlySearchBodyFields), diff --git a/routes/index.test.js b/routes/index.test.js index 0f6a072..482f09e 100644 --- a/routes/index.test.js +++ b/routes/index.test.js @@ -23,6 +23,12 @@ jest.mock('../utils/featureFlags', () => ({ isWebSearchEnabled: jest.fn(() => true) })); +const mockLimitSearchRequests = jest.fn((req, res, next) => next()); + +jest.mock('../utils/searchRateLimiter', () => ({ + createSearchRateLimiter: jest.fn(() => mockLimitSearchRequests) +})); + const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); const featureFlags = require('../utils/featureFlags'); @@ -35,6 +41,7 @@ describe('routes/index router', () => { app = createRouterApp(router); jest.clearAllMocks(); featureFlags.isWebSearchEnabled.mockReturnValue(true); + mockLimitSearchRequests.mockImplementation((req, res, next) => next()); }); test('GET /api/health-check returns 200', async () => { @@ -57,6 +64,7 @@ describe('routes/index router', () => { }); expect(response.status).toBe(200); + expect(mockLimitSearchRequests).toHaveBeenCalledTimes(1); expect(mw.validateParams).toHaveBeenCalledTimes(1); expect(controller.SEARCH).toHaveBeenCalledTimes(1); expect(response.body).toStrictEqual({ @@ -68,6 +76,28 @@ describe('routes/index router', () => { }); }); + test('POST /search returns a rate-limit response before validation and controller work', async () => { + mockLimitSearchRequests.mockImplementation((req, res) => + res.status(429).json({ + error: 'SEARCH_RATE_LIMITED', + message: 'Too many search requests. Please retry later.' + }) + ); + + const response = await request(app, { + method: 'POST', + path: '/search', + body: { + keywordSearch: 'test' + } + }); + + expect(response.status).toBe(429); + expect(response.body.error).toBe('SEARCH_RATE_LIMITED'); + expect(mw.validateParams).not.toHaveBeenCalled(); + expect(controller.SEARCH).not.toHaveBeenCalled(); + }); + test('GET /search is no longer mounted', async () => { const response = await request(app, { path: '/search?keywordSearch=test' @@ -230,6 +260,7 @@ describe('routes/index router', () => { }); expect(response.status).toBe(200); + expect(mockLimitSearchRequests).not.toHaveBeenCalled(); expect(controller.WEB_SEARCH).toHaveBeenCalledTimes(1); expect(response.body).toStrictEqual({ route: 'webSearch', diff --git a/utils/errors.js b/utils/errors.js index edd76be..6c4654d 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -255,6 +255,10 @@ class errors { searchTimeoutError() { return makeError('SEARCH_TIMEOUT', 'Search request timed out. Please try again later.'); } + + searchRateLimitedError() { + return makeError('SEARCH_RATE_LIMITED', 'Too many search requests. Please retry later.'); + } } module.exports = { diff --git a/utils/errors.test.js b/utils/errors.test.js index c73b512..494b1b2 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -190,6 +190,14 @@ describe('error response helpers', () => { message: 'Search request timed out. Please try again later.' } ], + [ + 'searchRateLimitedError', + [], + { + error: 'SEARCH_RATE_LIMITED', + message: 'Too many search requests. Please retry later.' + } + ], [ 'searchBodyError', [], diff --git a/utils/searchRateLimiter.js b/utils/searchRateLimiter.js new file mode 100644 index 0000000..00026e1 --- /dev/null +++ b/utils/searchRateLimiter.js @@ -0,0 +1,100 @@ +const { AppConfig } = require('cve-core'); +const { rateLimit } = require('express-rate-limit'); +const errors = require('./errors'); +const searchPerformance = require('./searchPerformance'); + +const DEFAULT_SEARCH_RATE_LIMIT_ENABLED = false; +const DEFAULT_SEARCH_RATE_LIMIT_WINDOW_MS = 60000; +const GLOBAL_SEARCH_RATE_LIMIT_KEY = 'search-api-process'; + +const error = new errors.errors(); + +function parseBoolean(value) { + if (typeof value === 'boolean') return value; + + const normalized = String(value).trim().toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false; + + return undefined; +} + +function readBooleanConfig(path, defaultValue) { + if (!AppConfig.has(path)) return defaultValue; + + const value = parseBoolean(AppConfig.get(path)); + if (value === undefined) throw new Error(`${path} must be a boolean.`); + + return value; +} + +function readPositiveIntegerConfig(path, defaultValue) { + if (!AppConfig.has(path)) return defaultValue; + + const value = Number(AppConfig.get(path)); + if (!Number.isInteger(value) || value < 1) return undefined; + + return value; +} + +function getSearchRateLimitConfig() { + const enabled = readBooleanConfig('rateLimiting.search.enabled', DEFAULT_SEARCH_RATE_LIMIT_ENABLED); + if (!enabled) { + return Object.freeze({ + enabled, + windowMs: DEFAULT_SEARCH_RATE_LIMIT_WINDOW_MS + }); + } + + const windowMs = readPositiveIntegerConfig('rateLimiting.search.windowMs', DEFAULT_SEARCH_RATE_LIMIT_WINDOW_MS); + const maxRequests = readPositiveIntegerConfig('rateLimiting.search.maxRequests'); + + if (!windowMs) throw new Error('rateLimiting.search.windowMs must be a positive integer when enabled.'); + if (!maxRequests) throw new Error('rateLimiting.search.maxRequests must be a positive integer when enabled.'); + + return Object.freeze({ + enabled, + windowMs, + maxRequests + }); +} + +function disabledRateLimiter(_req, _res, next) { + next(); +} + +function handleRateLimit(req, res, _next, options) { + searchPerformance.logSearchPerformance( + { + requestId: req.requestId, + outcome: 'rate_limited', + statusCode: options.statusCode + }, + undefined, + undefined, + undefined + ); + + return res.status(options.statusCode).json(error.searchRateLimitedError()); +} + +function createSearchRateLimiter(config = getSearchRateLimitConfig()) { + if (!config.enabled) return disabledRateLimiter; + + return rateLimit({ + windowMs: config.windowMs, + limit: config.maxRequests, + standardHeaders: 'draft-8', + legacyHeaders: false, + identifier: GLOBAL_SEARCH_RATE_LIMIT_KEY, + keyGenerator: () => GLOBAL_SEARCH_RATE_LIMIT_KEY, + handler: handleRateLimit, + passOnStoreError: false + }); +} + +module.exports = { + createSearchRateLimiter, + getSearchRateLimitConfig, + handleRateLimit +}; diff --git a/utils/searchRateLimiter.test.js b/utils/searchRateLimiter.test.js new file mode 100644 index 0000000..da00ea6 --- /dev/null +++ b/utils/searchRateLimiter.test.js @@ -0,0 +1,127 @@ +const express = require('express'); +const { request } = require('../test/http'); + +const mockAppConfigHas = jest.fn(); +const mockAppConfigGet = jest.fn(); +const mockLogSearchPerformance = jest.fn(); + +jest.mock('cve-core', () => ({ + AppConfig: { + get: mockAppConfigGet, + has: mockAppConfigHas + } +})); + +jest.mock('./searchPerformance', () => ({ + logSearchPerformance: mockLogSearchPerformance +})); + +const searchRateLimiter = require('./searchRateLimiter'); + +describe('searchRateLimiter', () => { + beforeEach(() => { + mockAppConfigHas.mockReset(); + mockAppConfigGet.mockReset(); + mockLogSearchPerformance.mockReset(); + mockAppConfigHas.mockReturnValue(false); + }); + + test('defaults to a disabled limiter that passes requests through', () => { + const next = jest.fn(); + const limiter = searchRateLimiter.createSearchRateLimiter(); + + limiter({}, {}, next); + + expect(next).toHaveBeenCalledTimes(1); + }); + + test('resolves enabled rate limit configuration from AppConfig', () => { + const values = { + 'rateLimiting.search.enabled': 'TRUE', + 'rateLimiting.search.windowMs': '30000', + 'rateLimiting.search.maxRequests': '25' + }; + mockAppConfigHas.mockImplementation((path) => Object.hasOwn(values, path)); + mockAppConfigGet.mockImplementation((path) => values[path]); + + expect(searchRateLimiter.getSearchRateLimitConfig()).toStrictEqual({ + enabled: true, + windowMs: 30000, + maxRequests: 25 + }); + }); + + test.each([ + [ + { + 'rateLimiting.search.enabled': 'invalid' + }, + 'rateLimiting.search.enabled must be a boolean.' + ], + [ + { + 'rateLimiting.search.enabled': 'TRUE', + 'rateLimiting.search.windowMs': '0', + 'rateLimiting.search.maxRequests': '25' + }, + 'rateLimiting.search.windowMs must be a positive integer when enabled.' + ], + [ + { + 'rateLimiting.search.enabled': 'TRUE', + 'rateLimiting.search.windowMs': '60000', + 'rateLimiting.search.maxRequests': '0' + }, + 'rateLimiting.search.maxRequests must be a positive integer when enabled.' + ] + ])('rejects invalid explicit configuration', (values, expectedMessage) => { + mockAppConfigHas.mockImplementation((path) => Object.hasOwn(values, path)); + mockAppConfigGet.mockImplementation((path) => values[path]); + + expect(() => searchRateLimiter.getSearchRateLimitConfig()).toThrow(expectedMessage); + }); + + test('enforces a process-wide limit with standard headers and sanitized telemetry', async () => { + const app = express(); + const limiter = searchRateLimiter.createSearchRateLimiter({ + enabled: true, + windowMs: 60000, + maxRequests: 1 + }); + + app.use((req, res, next) => { + req.requestId = 'request-123'; + next(); + }); + app.post('/search', limiter, (req, res) => res.status(200).json({ ok: true })); + + const accepted = await request(app, { + method: 'POST', + path: '/search' + }); + const rejected = await request(app, { + method: 'POST', + path: '/search' + }); + + expect(accepted.status).toBe(200); + expect(accepted.headers.ratelimit).toBeDefined(); + expect(accepted.headers['ratelimit-policy']).toBeDefined(); + expect(rejected.status).toBe(429); + expect(rejected.headers['retry-after']).toBeDefined(); + expect(rejected.body).toStrictEqual({ + error: 'SEARCH_RATE_LIMITED', + message: 'Too many search requests. Please retry later.' + }); + expect(mockLogSearchPerformance).toHaveBeenCalledWith( + { + requestId: 'request-123', + outcome: 'rate_limited', + statusCode: 429 + }, + undefined, + undefined, + undefined + ); + }); +}); From c77b297fca9a242b752d4473efbe3fa08da4665e Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Mon, 27 Jul 2026 15:01:03 -0400 Subject: [PATCH 07/20] Remediate owned dependency vulnerabilities --- package-lock.json | 40 ++++++++++++++++------------------------ package.json | 4 ++-- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca4464e..bae0a62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "express-rate-limit": "^8.6.1", "express-validator": "^7.2.1", "http-errors": "~1.8.1", - "morgan": "~1.10.0", + "morgan": "~1.11.0", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -1799,9 +1799,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -1858,15 +1858,15 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", + "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -4908,19 +4908,23 @@ } }, "node_modules/morgan": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", - "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", + "integrity": "sha512-zSkVu3t18r39pw4ixfBKvfZi3y2UOqr7d4WYwcj3m8nXpEQK4rPO6GLzs/CExoRgmX3y9EjmmcXqv6jq0SK46g==", "license": "MIT", "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", - "on-finished": "~2.3.0", + "on-finished": "~2.4.1", "on-headers": "~1.1.0" }, "engines": { "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/morgan/node_modules/debug": { @@ -4938,18 +4942,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index dd7d2f2..de45e6f 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "express-rate-limit": "^8.6.1", "express-validator": "^7.2.1", "http-errors": "~1.8.1", - "morgan": "~1.10.0", + "morgan": "~1.11.0", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -36,7 +36,7 @@ }, "overrides": { "axios": "1.18.0", - "brace-expansion": "5.0.6", + "brace-expansion": "5.0.8", "lodash": "4.18.1", "lodash.unset": "4.18.0", "path-to-regexp": "0.1.13", From 0347312b2e83e56c901c1833aaea30aa98fd754d Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 08:26:01 -0400 Subject: [PATCH 08/20] fix: enforce CPE vulnerability and range semantics --- README.md | 4 ++- api-docs/openapi.json | 8 ++--- api-docs/openapi.test.js | 3 ++ controllers/middleware.js | 12 +++++++ controllers/middleware.test.js | 49 ++++++++++++++++++++++++++++ controllers/searchController.js | 7 +++- controllers/searchController.test.js | 33 +++++++++++++++++-- routes/index.js | 3 ++ routes/search.integration.test.js | 22 +++++++++++++ routes/swagger.js | 6 ++-- utils/cpe.js | 13 +++++--- utils/cpe.test.js | 26 +++++++++++++++ utils/errors.js | 7 ++++ utils/errors.test.js | 9 +++++ utils/searchQueryBuilder.js | 17 ++++++++++ utils/searchQueryBuilder.test.js | 31 ++++++++++++++++++ 16 files changed, 234 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index bb6300a..045df3b 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ curl -X POST "http://localhost:3000/search" \ The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. +Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Criteria entry itself to be marked vulnerable. A vulnerable flag on an unrelated CPE entry does not satisfy the request. Omitting `isVulnerable`, or setting it to `false`, does not apply a vulnerability restriction. + ### Virtual Match String Search Use `virtualMatchString` when a user needs NVD-style CPE Match Criteria compatibility matching instead of concrete asset matching. Omitted trailing components and `*` components act as broad matches, matching NVD's broad CPE Match String behavior. @@ -150,7 +152,7 @@ curl -X POST "http://localhost:3000/search" \ ### Virtual Match String Version Ranges -Use `versionStart`, `versionStartType`, `versionEnd`, and `versionEndType` with `virtualMatchString` to search CPE Match String ranges. Each provided version bound must include its matching type, and the `virtualMatchString` must be a partial CPE 2.3 match string without a version component. +Use `versionStart`, `versionStartType`, `versionEnd`, and `versionEndType` with `virtualMatchString` to search CPE Match String ranges. Each provided version bound must include its matching type, and the `virtualMatchString` must be a partial CPE 2.3 match string without a version component. When both bounds are provided, `versionStart` must be less than `versionEnd`; equal values are valid only when both bounds are `including`. ```bash curl -X POST "http://localhost:3000/search" \ diff --git a/api-docs/openapi.json b/api-docs/openapi.json index eea8e4d..5b08a56 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -36,7 +36,7 @@ } }, "400": { - "description": "Bad Request" + "description": "Invalid request body, parameter combination, or version range" }, "404": { "description": "Invalid cpeName value" @@ -368,7 +368,7 @@ "isVulnerable": { "type": "boolean", "default": false, - "description": "When true, returns CVE records whose CPE Match Criteria match cpeName and are marked vulnerable. cpeName is required when isVulnerable is true. virtualMatchString cannot be provided with isVulnerable." + "description": "When true, returns CVE records where the same CPE Match Criteria entry both matches cpeName and is marked vulnerable. False or omitted does not apply a vulnerability restriction. cpeName is required when isVulnerable is true. virtualMatchString cannot be provided with isVulnerable." }, "lastModStartDate": { "type": "string", @@ -412,7 +412,7 @@ "versionStart": { "type": "string", "example": "2.2", - "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. virtualMatchString must not include a version component." + "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." }, "versionStartType": { "type": "string", @@ -425,7 +425,7 @@ "versionEnd": { "type": "string", "example": "2.6", - "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. virtualMatchString must not include a version component." + "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." }, "versionEndType": { "type": "string", diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 1083eb7..2ce49c8 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -62,11 +62,14 @@ describe('OpenAPI contract', () => { expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_IDS} CVE IDs`); expect(schema.properties.noRejected.type).toBe('boolean'); expect(schema.properties.noRejected.description).toContain('When true'); + expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry'); + expect(schema.properties.isVulnerable.description).toContain('False or omitted'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); expect(schema.properties.cveId).toBeUndefined(); expect(schema.properties.versionStart.description).toContain('versionStartType'); + expect(schema.properties.versionStart.description).toContain('less than versionEnd'); expect(schema.properties.versionEnd.description).toContain('versionEndType'); expect(schema.properties.virtualMatchString.description).toContain('CPE Match Criteria'); expect(schema.properties.virtualMatchString.description).toContain('compatible'); diff --git a/controllers/middleware.js b/controllers/middleware.js index 446634f..4a3ea66 100644 --- a/controllers/middleware.js +++ b/controllers/middleware.js @@ -193,6 +193,18 @@ function validateParams(req, res, next) { if (virtualMatchString.hasVersionComponent) { return res.status(400).json(error.virtualMatchStringVersionComponentError()); } + + if (hasParam(params, 'versionStart') && hasParam(params, 'versionEnd')) { + const comparison = cpe.compareCpeVersions(params.versionStart, params.versionEnd); + const hasEmptyEqualRange = + comparison === 0 && + (params.versionStartType.toLowerCase() !== 'including' || + params.versionEndType.toLowerCase() !== 'including'); + + if (comparison > 0 || hasEmptyEqualRange) { + return res.status(400).json(error.versionRangeOrderError()); + } + } } for (const { param, allowed, buildError } of severityValidators) { diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index c4df38f..0a83745 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -172,6 +172,55 @@ describe('middleware validation function', () => { expectValidationSuccess(); }); + test('ValidateParams returns an error when versionStart exceeds versionEnd', () => { + validate({ + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + versionStart: '3.0', + versionStartType: 'including', + versionEnd: '2.0', + versionEndType: 'including' + }); + + expectValidationError( + 'VERSION_RANGE_ORDER', + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + ); + }); + + test.each([ + ['excluding', 'including'], + ['including', 'excluding'], + ['excluding', 'excluding'] + ])( + 'ValidateParams returns an error for an empty equal version range with %s/%s bounds', + (versionStartType, versionEndType) => { + validate({ + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + versionStart: '2.0', + versionStartType, + versionEnd: '2.0', + versionEndType + }); + + expectValidationError( + 'VERSION_RANGE_ORDER', + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + ); + } + ); + + test('ValidateParams accepts an equal inclusive version range', () => { + validate({ + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + versionStart: '2.0', + versionStartType: 'including', + versionEnd: '2.0', + versionEndType: 'including' + }); + + expectValidationSuccess(); + }); + test('ValidateParams returns an error when version range params are used without virtualMatchString', () => { validate({ versionStart: '2.2', diff --git a/controllers/searchController.js b/controllers/searchController.js index bf2eb1a..f1da50d 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -33,7 +33,12 @@ function filterSearchHits(resp, postFilters) { hitsContainer, candidateCount: hitsContainer.hits.length, filteredHits: hitsContainer.hits.filter((hit) => { - if (postFilters?.cpeName && !cpe.hitAppliesToCpeName(hit, postFilters.cpeName, cpeEvaluationContext)) { + if ( + postFilters?.cpeName && + !cpe.hitAppliesToCpeName(hit, postFilters.cpeName, cpeEvaluationContext, { + requireVulnerable: postFilters.isVulnerable === true + }) + ) { return false; } diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 64f8648..51673ca 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -341,6 +341,7 @@ describe('Search controller SEARCH handler', () => { expect(mockSearchRaw.mock.calls[0][0]._source).toStrictEqual([ 'cveMetadata.cveId', 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria', + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartIncluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartExcluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionEndIncluding', @@ -419,8 +420,8 @@ describe('Search controller SEARCH handler', () => { } }); expect(filters[2]).toStrictEqual({ - match_phrase: { - 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable': 'true' + term: { + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable': true } }); expect(body.size).toBe(10000); @@ -428,6 +429,34 @@ describe('Search controller SEARCH handler', () => { expect(mockApiSearch).not.toHaveBeenCalled(); }); + test('SEARCH requires the matching CPE entry itself to be vulnerable', async () => { + const matchingNotVulnerable = cpeHit('CVE-2024-0001', { + criteria: 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*', + vulnerable: false + }); + matchingNotVulnerable._source.containers.cna.cpeApplicability.nodes[0].cpeMatch.push({ + criteria: 'cpe:2.3:a:other:product:1.0:*:*:*:*:*:*:*', + vulnerable: true + }); + mockSearchRaw.mockResolvedValue(rawResponse(1, [matchingNotVulnerable])); + + await controller.SEARCH( + { + query: { + cpeName: 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*', + isVulnerable: true + } + }, + res, + next + ); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json.mock.calls[0][0].data.hits.total.value).toBe(0); + expect(res.json.mock.calls[0][0].data.hits.hits).toStrictEqual([]); + expect(mockApiSearch).not.toHaveBeenCalled(); + }); + test('SEARCH applies semantic CPE version filtering before building response metadata', async () => { const applicable = cpeHit('CVE-2024-0001', { criteria: 'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*', diff --git a/routes/index.js b/routes/index.js index 7604253..f221274 100644 --- a/routes/index.js +++ b/routes/index.js @@ -206,6 +206,9 @@ headers: { } } } +#swagger.responses[400] = { +description: 'Invalid request body, parameter combination, or version range', +} #swagger.responses[404] = { description: 'Invalid cpeName value', } diff --git a/routes/search.integration.test.js b/routes/search.integration.test.js index 740f9ad..e153c61 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -106,6 +106,28 @@ describe('search route integration', () => { expect(mockApiSearch).not.toHaveBeenCalled(); }); + test('POST /search rejects an inverted virtualMatchString version range before invoking OpenSearch', async () => { + const response = await request(createApp(), { + method: 'POST', + path: '/search', + body: { + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + versionStart: '3.0', + versionStartType: 'including', + versionEnd: '2.0', + versionEndType: 'including' + } + }); + + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: 'VERSION_RANGE_ORDER', + message: + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + }); + expect(mockApiSearch).not.toHaveBeenCalled(); + }); + test('POST /webSearch normalizes body fields and formats BasicSearchManager responses', async () => { mockBasicSearch.mockResolvedValue(basicResponse({ total: 1, hits: [{ _id: 'CVE-2024-0002' }] })); diff --git a/routes/swagger.js b/routes/swagger.js index ea6f64f..76e8c2e 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -130,7 +130,7 @@ const searchRequestProperties = { } ), isVulnerable: searchRequestProperty( - 'When true, returns CVE records whose CPE Match Criteria match cpeName and are marked vulnerable. cpeName is required when isVulnerable is true. virtualMatchString cannot be provided with isVulnerable.', + 'When true, returns CVE records where the same CPE Match Criteria entry both matches cpeName and is marked vulnerable. False or omitted does not apply a vulnerability restriction. cpeName is required when isVulnerable is true. virtualMatchString cannot be provided with isVulnerable.', { type: 'boolean', default: false @@ -188,7 +188,7 @@ const searchRequestProperties = { } ), versionStart: searchRequestProperty( - 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. virtualMatchString must not include a version component.', + 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', { type: 'string', example: '2.2' @@ -202,7 +202,7 @@ const searchRequestProperties = { } ), versionEnd: searchRequestProperty( - 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. virtualMatchString must not include a version component.', + 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', { type: 'string', example: '2.6' diff --git a/utils/cpe.js b/utils/cpe.js index c4f3bc5..293daf8 100644 --- a/utils/cpe.js +++ b/utils/cpe.js @@ -245,10 +245,12 @@ function compareCpeVersions(a, b) { const tokenB = tokensB[i]; if (tokenA === undefined) { - return /^0+$/.test(tokenB) ? 0 : -1; + if (/^0+$/.test(tokenB)) continue; + return -1; } if (tokenB === undefined) { - return /^0+$/.test(tokenA) ? 0 : 1; + if (/^0+$/.test(tokenA)) continue; + return 1; } if (tokenA === tokenB) continue; @@ -365,11 +367,12 @@ function rangeBoundaryMatches(assetVersion, cpeMatch) { return true; } -function cpeMatchAppliesToCpeName(cpeMatch, cpeName, context) { +function cpeMatchAppliesToCpeName(cpeMatch, cpeName, context, { requireVulnerable = false } = {}) { const asset = cpeName?.valid ? cpeName : parseCpeName(cpeName); const criteria = getParsedCpeMatchCriteria(cpeMatch?.criteria, context); if (!asset.valid || !criteria.valid) return false; + if (requireVulnerable && cpeMatch?.vulnerable !== true) return false; for (const index of [0, 1, 2]) { if (!componentMatches(asset.components[index], criteria.components[index])) { @@ -453,8 +456,8 @@ function getCpeMatchesFromHit(hit) { return matches; } -function hitAppliesToCpeName(hit, cpeName, context) { - return getCpeMatchesFromHit(hit).some((match) => cpeMatchAppliesToCpeName(match, cpeName, context)); +function hitAppliesToCpeName(hit, cpeName, context, options) { + return getCpeMatchesFromHit(hit).some((match) => cpeMatchAppliesToCpeName(match, cpeName, context, options)); } function hitOverlapsVirtualMatchStringRange(hit, virtualMatchStringRange, context) { diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 5344929..041728e 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -159,6 +159,10 @@ describe('CPE helpers', () => { test('compareCpeVersions compares numeric version segments semantically', () => { expect(cpe.compareCpeVersions('2.10', '2.9')).toBeGreaterThan(0); expect(cpe.compareCpeVersions('10.05.1', '10.5.1')).toBe(0); + expect(cpe.compareCpeVersions('1', '1.0')).toBe(0); + expect(cpe.compareCpeVersions('1.0.0', '1')).toBe(0); + expect(cpe.compareCpeVersions('1', '1.0.1')).toBeLessThan(0); + expect(cpe.compareCpeVersions('1.0.1', '1')).toBeGreaterThan(0); expect(cpe.compareCpeVersions('1.0', '1.0.1')).toBeLessThan(0); }); @@ -187,6 +191,28 @@ describe('CPE helpers', () => { ).toBe(false); }); + test('cpeMatchAppliesToCpeName requires vulnerability on the matching entry when requested', () => { + const cpeName = 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*'; + const matchingCriteria = 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*'; + + expect( + cpe.cpeMatchAppliesToCpeName({ criteria: matchingCriteria, vulnerable: true }, cpeName, undefined, { + requireVulnerable: true + }) + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName({ criteria: matchingCriteria, vulnerable: false }, cpeName, undefined, { + requireVulnerable: true + }) + ).toBe(false); + expect( + cpe.cpeMatchAppliesToCpeName({ criteria: matchingCriteria }, cpeName, undefined, { + requireVulnerable: true + }) + ).toBe(false); + expect(cpe.cpeMatchAppliesToCpeName({ criteria: matchingCriteria, vulnerable: false }, cpeName)).toBe(true); + }); + test('cpeMatchOverlapsVirtualMatchStringRange matches exact criteria inside the requested range', () => { const virtualMatchStringRange = { matchString: cpe.parseVirtualMatchString('cpe:2.3:o:linux:linux_kernel'), diff --git a/utils/errors.js b/utils/errors.js index 6c4654d..ef6612f 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -70,6 +70,13 @@ class errors { return makeError('VERSION_RANGE_TYPE', `The ${param} parameter must be including or excluding.`); } + versionRangeOrderError() { + return makeError( + 'VERSION_RANGE_ORDER', + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + ); + } + virtualMatchStringVersionRangeFormatError() { return makeError( 'VIRTUAL_MATCH_STRING_RANGE_FORMAT', diff --git a/utils/errors.test.js b/utils/errors.test.js index 494b1b2..1f19c5a 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -99,6 +99,15 @@ describe('error response helpers', () => { message: 'The versionEndType parameter must be including or excluding.' } ], + [ + 'versionRangeOrderError', + [], + { + error: 'VERSION_RANGE_ORDER', + message: + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + } + ], [ 'virtualMatchStringVersionRangeFormatError', [], diff --git a/utils/searchQueryBuilder.js b/utils/searchQueryBuilder.js index b2e1534..83104ce 100644 --- a/utils/searchQueryBuilder.js +++ b/utils/searchQueryBuilder.js @@ -10,6 +10,7 @@ const modDatePath = 'cveMetadata.dateUpdated'; const cpeCandidateSourceFields = [ 'cveMetadata.cveId', 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria', + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartIncluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartExcluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionEndIncluding', @@ -97,6 +98,7 @@ function buildPostFilters(params) { const virtualMatchStringRangePostFilter = getVirtualMatchStringRangePostFilter(params); if (cpeNamePostFilter) postFilters.cpeName = cpeNamePostFilter; + if (cpeNamePostFilter && isTrueValue(params?.isVulnerable)) postFilters.isVulnerable = true; if (virtualMatchStringRangePostFilter) postFilters.virtualMatchStringRange = virtualMatchStringRangePostFilter; else if (virtualMatchStringPostFilter) postFilters.virtualMatchString = virtualMatchStringPostFilter; @@ -204,6 +206,10 @@ function processParams(params) { return allParams; } +function isTrueValue(value) { + return value === true || String(value).toLowerCase() === 'true'; +} + function addWildCard(searchString) { const terms = String(searchString ?? '') .trim() @@ -293,6 +299,17 @@ function getFilterFields(params) { return; } + if (param === 'isVulnerable') { + if (isTrueValue(params[param])) { + fieldsToReturn.push({ + term: { + [fieldSpec]: true + } + }); + } + return; + } + if (param === 'virtualMatchString') { const filter = buildVirtualMatchStringFilter(params[param]); if (filter) fieldsToReturn.push(filter); diff --git a/utils/searchQueryBuilder.test.js b/utils/searchQueryBuilder.test.js index 4f09cfd..2c214bc 100644 --- a/utils/searchQueryBuilder.test.js +++ b/utils/searchQueryBuilder.test.js @@ -90,6 +90,36 @@ describe('searchQueryBuilder buildSearchRequest()', () => { expect(output.postFilters.cpeName.version).toBe('2.10'); }); + test('buildSearchRequest preserves true isVulnerable for same-entry semantic filtering', () => { + const output = queryBuilder.buildSearchRequest({ + cpeName: 'cpe:2.3:a:vendor:product:2.10:*:*:*:*:*:*:*', + isVulnerable: true + }); + + expect(output.postFilters.isVulnerable).toBe(true); + expect(output.searchOptions.filters).toContainEqual({ + term: { + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable': true + } + }); + }); + + test('buildSearchRequest treats false isVulnerable as no restriction', () => { + const output = queryBuilder.buildSearchRequest({ + cpeName: 'cpe:2.3:a:vendor:product:2.10:*:*:*:*:*:*:*', + isVulnerable: false + }); + + expect(output.postFilters.isVulnerable).toBeUndefined(); + expect(output.searchOptions.filters).not.toContainEqual( + expect.objectContaining({ + term: expect.objectContaining({ + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable': expect.anything() + }) + }) + ); + }); + test('buildSearchRequest over-fetches virtualMatchString candidates for semantic post-filtering', () => { const output = queryBuilder.buildSearchRequest({ virtualMatchString: 'cpe:2.3:o:microsoft:windows_10_1809:*:*:*:*:*:*:x86:*', @@ -189,6 +219,7 @@ describe('searchQueryBuilder buildSearchRequest()', () => { _source: [ 'cveMetadata.cveId', 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria', + 'containers.cna.cpeApplicability.nodes.cpeMatch.vulnerable', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartIncluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionStartExcluding', 'containers.cna.cpeApplicability.nodes.cpeMatch.versionEndIncluding', From 7415ffc94e21cc64ed805b8c4dc5b5cd5f73aa7d Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 08:58:44 -0400 Subject: [PATCH 09/20] fix: harden search request and response handling --- README.md | 2 +- api-docs/openapi.json | 16 +++++++++++- api-docs/openapi.test.js | 14 ++++++++++ controllers/searchController.js | 30 ++++++++++++++++----- controllers/searchController.test.js | 39 +++++++++++++++++++++++----- routes/index.js | 31 +++++++++++++++++++++- routes/index.test.js | 36 +++++++++++++++++++++++++ server.js | 18 ++++++++++++- server.test.js | 38 +++++++++++++++++++++++++++ test/http.js | 30 ++++++++++++--------- utils/errors.js | 11 ++++++++ utils/errors.test.js | 16 ++++++++++++ 12 files changed, 252 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 045df3b..9e18b1c 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ The Search API will be served at `localhost:3000`. ## API Input Validation -`POST /search` validates supported JSON request body fields, boolean parameters, pagination values, CVSS severity values, required parameter combinations, CPE Name requirements, and date-time formats. +`POST /search` accepts an optional JSON object request body and validates supported fields, boolean parameters, pagination values, CVSS severity values, required parameter combinations, CPE Name requirements, and date-time formats. A non-empty body must use the `application/json` media type with UTF-8 encoding; unsupported media types or encodings return HTTP 415, and malformed JSON returns HTTP 400. If OpenSearch is unavailable, the endpoint returns HTTP 503 with a `Retry-After` header. Use `cveIds` to request up to 100 CVE records by comma-separated CVE ID. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 5b08a56..cbf91eb 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -36,11 +36,14 @@ } }, "400": { - "description": "Invalid request body, parameter combination, or version range" + "description": "Malformed JSON, invalid request body, parameter combination, or version range" }, "404": { "description": "Invalid cpeName value" }, + "415": { + "description": "A non-empty request body did not use the application/json media type with UTF-8 encoding" + }, "429": { "description": "Application rate limit exceeded, or the cold CPE search queue is full or exceeded its maximum wait", "headers": { @@ -64,6 +67,17 @@ } } }, + "503": { + "description": "OpenSearch is unavailable", + "headers": { + "Retry-After": { + "description": "Seconds before retrying an unavailable OpenSearch dependency", + "schema": { + "type": "integer" + } + } + } + }, "504": { "description": "Search exceeded its OpenSearch or overall processing deadline" } diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 2ce49c8..713c042 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -114,6 +114,20 @@ describe('OpenAPI contract', () => { expect(openapi.paths['/search'].post.responses['429'].description).toContain('rate limit'); }); + test('documents fail-closed request and dependency responses for POST /search', () => { + const responses = openapi.paths['/search'].post.responses; + + expect(responses['400'].description).toContain('Malformed JSON'); + expect(responses['415'].description).toContain('application/json'); + expect(responses['503'].description).toContain('OpenSearch'); + expect(responses['503'].headers['Retry-After']).toStrictEqual({ + description: 'Seconds before retrying an unavailable OpenSearch dependency', + schema: { + type: 'integer' + } + }); + }); + test('documents request correlation headers', () => { expect(openapi.paths['/search'].post.responses['200'].headers['X-Request-ID']).toStrictEqual({ description: 'Request correlation ID', diff --git a/controllers/searchController.js b/controllers/searchController.js index f1da50d..3ec5c52 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -8,6 +8,8 @@ const searchPostFilterWorkQueue = require('../utils/searchPostFilterWorkQueue'); const searchProvider = require('../utils/searchProvider'); const searchQueryBuilder = require('../utils/searchQueryBuilder'); const error = new errors.errors(); +const openSearchUnavailableErrorNames = new Set(['ConnectionError', 'NoLivingConnectionsError']); +const openSearchUnavailableStatusCodes = new Set([502, 503, 504]); function hasSearchPostFilters(postFilters) { return Boolean(postFilters?.cpeName || postFilters?.virtualMatchString || postFilters?.virtualMatchStringRange); @@ -96,6 +98,17 @@ function buildPostFilteredResponse( return buildPagedSearchResponse(resp, filteredHits.length, pagedHits); } +function getOpenSearchErrorStatusCode(err) { + return Number(err?.statusCode ?? err?.meta?.statusCode ?? err?.meta?.body?.status); +} + +function isOpenSearchUnavailableError(err) { + return ( + openSearchUnavailableErrorNames.has(err?.name) || + openSearchUnavailableStatusCodes.has(getOpenSearchErrorStatusCode(err)) + ); +} + function buildWebSearchSort(sort) { if (!sort) return undefined; @@ -359,9 +372,10 @@ async function searchHandler(req, res, next) { //Calculate current page and total pages to append to JSON output const totalValue = resp.data.hits.total.value; - let pageSize = Math.min(totalValue, pagingOptions.resultsPerPage); - const page = pageSize === 0 ? 0 : Math.floor(pagingOptions.from / pageSize) + 1; - const totalPages = pageSize === 0 ? 0 : Math.ceil(totalValue / pageSize); + const requestedPageSize = pagingOptions.resultsPerPage; + const pageSize = Math.min(totalValue, requestedPageSize); + const page = totalValue === 0 ? 0 : Math.floor(pagingOptions.from / requestedPageSize) + 1; + const totalPages = totalValue === 0 ? 0 : Math.ceil(totalValue / requestedPageSize); return res.status(200).json({ page, @@ -396,10 +410,12 @@ async function searchHandler(req, res, next) { return res.status(504).json(error.searchTimeoutError()); } - if (err?.name === 'ConnectionError') { - metrics.outcome = 'connection_error'; - metrics.statusCode = 400; - return res.status(400).json(error.openSearchError()); + if (isOpenSearchUnavailableError(err)) { + metrics.outcome = 'opensearch_unavailable'; + metrics.statusCode = 503; + if (typeof res.set === 'function') res.set('Retry-After', '5'); + + return res.status(503).json(error.openSearchError()); } metrics.outcome = 'upstream_error'; diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 51673ca..750f0d4 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -1223,10 +1223,36 @@ describe('Search controller SEARCH handler', () => { }); }); - test('SEARCH returns openSearchError when cve-core throws a ConnectionError', async () => { - mockApiSearch.mockRejectedValue({ - name: 'ConnectionError' + test('SEARCH calculates an out-of-range page from the requested page size', async () => { + const resp = advancedResponse(5); + mockApiSearch.mockResolvedValue(resp); + + await controller.SEARCH( + { + query: { + resultsPerPage: 10, + pageNumber: 2 + } + }, + res, + next + ); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith({ + page: 2, + pageSize: 5, + totalPages: 1, + ...resp }); + }); + + test.each([ + ['ConnectionError', { name: 'ConnectionError' }], + ['NoLivingConnectionsError', { name: 'NoLivingConnectionsError' }], + ['OpenSearch 503 response', { name: 'ResponseError', meta: { statusCode: 503 } }] + ])('SEARCH returns a retryable 503 when OpenSearch is unavailable through %s', async (_caseName, thrownError) => { + mockApiSearch.mockRejectedValue(thrownError); await controller.SEARCH( { @@ -1237,7 +1263,8 @@ describe('Search controller SEARCH handler', () => { next ); - expect(res.status).toHaveBeenCalledWith(400); + expect(res.status).toHaveBeenCalledWith(503); + expect(res.set).toHaveBeenCalledWith('Retry-After', '5'); expect(res.json).toHaveBeenCalledWith({ error: 'OPEN_SEARCH_ERROR', message: @@ -1246,8 +1273,8 @@ describe('Search controller SEARCH handler', () => { expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ event: 'search.performance', requestId: 'connection-error-request', - outcome: 'connection_error', - statusCode: 400 + outcome: 'opensearch_unavailable', + statusCode: 503 }); }); diff --git a/routes/index.js b/routes/index.js index f221274..787abaa 100644 --- a/routes/index.js +++ b/routes/index.js @@ -95,6 +95,20 @@ function rejectSearchQueryParams(req, res, next) { next(); } +function hasRequestBody(req) { + const contentLength = Number(req.headers['content-length']); + + return req.headers['transfer-encoding'] !== undefined || (Number.isFinite(contentLength) && contentLength > 0); +} + +function requireSearchJsonContentType(req, res, next) { + if (hasRequestBody(req) && !req.is('application/json')) { + return res.status(415).json(error.searchContentTypeError()); + } + + next(); +} + function requireWebSearchEnabled(req, res, next) { if (!featureFlags.isWebSearchEnabled()) { return res.status(410).json(error.webSearchDisabledError()); @@ -207,11 +221,14 @@ headers: { } } #swagger.responses[400] = { -description: 'Invalid request body, parameter combination, or version range', +description: 'Malformed JSON, invalid request body, parameter combination, or version range', } #swagger.responses[404] = { description: 'Invalid cpeName value', } +#swagger.responses[415] = { +description: 'A non-empty request body did not use the application/json media type with UTF-8 encoding', +} #swagger.responses[429] = { description: 'Application rate limit exceeded, or the cold CPE search queue is full or exceeded its maximum wait', headers: { @@ -235,10 +252,22 @@ headers: { } } } +#swagger.responses[503] = { +description: 'OpenSearch is unavailable', +headers: { + "Retry-After": { + description: "Seconds before retrying an unavailable OpenSearch dependency", + schema: { + type: "integer" + } + } +} +} #swagger.responses[504] = { description: 'Search exceeded its OpenSearch or overall processing deadline', } */ + requireSearchJsonContentType, limitSearchRequests, rejectSearchQueryParams, body().custom(isSearchBodyObject).withMessage(error.searchBodyError()), diff --git a/routes/index.test.js b/routes/index.test.js index 482f09e..dc12c74 100644 --- a/routes/index.test.js +++ b/routes/index.test.js @@ -76,6 +76,42 @@ describe('routes/index router', () => { }); }); + test('POST /search permits an omitted request body', async () => { + const response = await request(app, { + method: 'POST', + path: '/search' + }); + + expect(response.status).toBe(200); + expect(mockLimitSearchRequests).toHaveBeenCalledTimes(1); + expect(mw.validateParams).toHaveBeenCalledTimes(1); + expect(controller.SEARCH).toHaveBeenCalledTimes(1); + expect(response.body).toStrictEqual({ + route: 'search', + params: {} + }); + }); + + test('POST /search rejects a non-empty body with an unsupported media type before rate limiting', async () => { + const response = await request(app, { + method: 'POST', + path: '/search', + rawBody: '{"keywordSearch":"test"}', + headers: { + 'Content-Type': 'text/plain' + } + }); + + expect(response.status).toBe(415); + expect(response.body).toStrictEqual({ + error: 'SEARCH_CONTENT_TYPE', + message: 'The /search request body must use the application/json media type with UTF-8 encoding.' + }); + expect(mockLimitSearchRequests).not.toHaveBeenCalled(); + expect(mw.validateParams).not.toHaveBeenCalled(); + expect(controller.SEARCH).not.toHaveBeenCalled(); + }); + test('POST /search returns a rate-limit response before validation and controller work', async () => { mockLimitSearchRequests.mockImplementation((req, res) => res.status(429).json({ diff --git a/server.js b/server.js index d76f476..4c94a56 100644 --- a/server.js +++ b/server.js @@ -7,10 +7,13 @@ const http = require('http'); const logger = require('morgan'); const path = require('path'); const router = require('./routes/index'); +const errors = require('./utils/errors'); const requestLogging = require('./utils/requestLogging'); dotenv.load(); +const error = new errors.errors(); + const swaggerSetupOptions = { swaggerOptions: { supportedSubmitMethods: [] @@ -35,19 +38,32 @@ function createRequestLogger() { }); } +function handleRequestError(err, req, res, next) { + if (err?.type === 'entity.parse.failed') { + return res.status(400).json(error.malformedJsonError()); + } + + if (err?.status === 415 && req.path === '/search') { + return res.status(415).json(error.searchContentTypeError()); + } + + next(err); +} + function createApp() { const app = express(); app.use(requestLogging.assignRequestId); app.use(createRequestLogger()); + app.use(cors()); app.use(express.static(path.join(__dirname, 'public'))); app.use(express.json()); - app.use(cors()); app.get('/api-docs/openapi.json', (req, res) => res.json(openApiSpecification)); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiSpecification, swaggerSetupOptions)); app.use('/', router); + app.use(handleRequestError); return app; } diff --git a/server.test.js b/server.test.js index 377fb70..51ce3c8 100644 --- a/server.test.js +++ b/server.test.js @@ -142,6 +142,44 @@ describe('server application bootstrap', () => { }); }); + test('createApp returns a sanitized error for malformed JSON', async () => { + const response = await request(createApp(), { + method: 'POST', + path: '/search', + rawBody: '{"keywordSearch":', + headers: { + 'Content-Type': 'application/json' + } + }); + + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: 'MALFORMED_JSON', + message: 'The request body contains malformed JSON.' + }); + expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(mockSearch).not.toHaveBeenCalled(); + }); + + test('createApp returns a sanitized error for unsupported JSON encoding', async () => { + const response = await request(createApp(), { + method: 'POST', + path: '/search', + rawBody: '{"keywordSearch":"apache"}', + headers: { + 'Content-Type': 'application/json; charset=iso-8859-1' + } + }); + + expect(response.status).toBe(415); + expect(response.body).toStrictEqual({ + error: 'SEARCH_CONTENT_TYPE', + message: 'The /search request body must use the application/json media type with UTF-8 encoding.' + }); + expect(response.headers['access-control-allow-origin']).toBe('*'); + expect(mockSearch).not.toHaveBeenCalled(); + }); + test('getRuntimeConfig preserves baseURL behavior when PORT is set without BASE_URL', () => { delete process.env.BASE_URL; process.env.PORT = '4567'; diff --git a/test/http.js b/test/http.js index f437097..716afe9 100644 --- a/test/http.js +++ b/test/http.js @@ -8,10 +8,24 @@ function createRouterApp(router) { return app; } -function request(app, { method = 'GET', path, body, headers = {} }) { +function hasHeader(headers, name) { + return Object.keys(headers).some((header) => header.toLowerCase() === name.toLowerCase()); +} + +function request(app, { method = 'GET', path, body, rawBody, headers = {} }) { return new Promise((resolve, reject) => { const server = app.listen(0, () => { - const payload = body ? JSON.stringify(body) : undefined; + const payload = + rawBody !== undefined ? String(rawBody) : body !== undefined ? JSON.stringify(body) : undefined; + const requestHeaders = { ...headers }; + if (payload !== undefined) { + if (rawBody === undefined && !hasHeader(requestHeaders, 'content-type')) { + requestHeaders['Content-Type'] = 'application/json'; + } + if (!hasHeader(requestHeaders, 'content-length')) { + requestHeaders['Content-Length'] = Buffer.byteLength(payload); + } + } const { port } = server.address(); const req = http.request( { @@ -19,15 +33,7 @@ function request(app, { method = 'GET', path, body, headers = {} }) { port, path, method, - headers: { - ...headers, - ...(payload - ? { - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload) - } - : {}) - } + headers: requestHeaders }, (res) => { let text = ''; @@ -61,7 +67,7 @@ function request(app, { method = 'GET', path, body, headers = {} }) { server.close(() => reject(err)); }); - if (payload) { + if (payload !== undefined) { req.write(payload); } diff --git a/utils/errors.js b/utils/errors.js index ef6612f..73b5a8a 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -128,6 +128,17 @@ class errors { return makeError('SEARCH_BODY_OBJECT', 'The /search request body must be a JSON object.'); } + searchContentTypeError() { + return makeError( + 'SEARCH_CONTENT_TYPE', + 'The /search request body must use the application/json media type with UTF-8 encoding.' + ); + } + + malformedJsonError() { + return makeError('MALFORMED_JSON', 'The request body contains malformed JSON.'); + } + searchQueryUnsupportedError() { return makeError( 'SEARCH_QUERY_UNSUPPORTED', diff --git a/utils/errors.test.js b/utils/errors.test.js index 1f19c5a..f68c4f5 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -215,6 +215,22 @@ describe('error response helpers', () => { message: 'The /search request body must be a JSON object.' } ], + [ + 'searchContentTypeError', + [], + { + error: 'SEARCH_CONTENT_TYPE', + message: 'The /search request body must use the application/json media type with UTF-8 encoding.' + } + ], + [ + 'malformedJsonError', + [], + { + error: 'MALFORMED_JSON', + message: 'The request body contains malformed JSON.' + } + ], [ 'searchQueryUnsupportedError', [], From 71537e09fc48d2b28876b22d026c36d8e6e68e4a Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 09:09:21 -0400 Subject: [PATCH 10/20] fix: bound search input complexity --- README.md | 15 +++++ api-docs/openapi.json | 12 +++- api-docs/openapi.test.js | 7 ++ controllers/middleware.js | 22 ++++++ controllers/middleware.test.js | 107 ++++++++++++++++++++++++++++++ routes/search.integration.test.js | 20 ++++++ routes/swagger.js | 12 +++- utils/constants.js | 15 +++++ utils/errors.js | 15 +++++ 9 files changed, 221 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9e18b1c..65ccf31 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,21 @@ The Search API will be served at `localhost:3000`. `POST /search` accepts an optional JSON object request body and validates supported fields, boolean parameters, pagination values, CVSS severity values, required parameter combinations, CPE Name requirements, and date-time formats. A non-empty body must use the `application/json` media type with UTF-8 encoding; unsupported media types or encodings return HTTP 415, and malformed JSON returns HTTP 400. If OpenSearch is unavailable, the endpoint returns HTTP 503 with a `Retry-After` header. +String inputs are bounded before query construction: + +| Field | Limit | +| ---------------------------- | ---------------------------------------------------------------------: | +| `keywordSearch` | 512 characters and 64 whitespace-delimited terms | +| `cveIds` | 4,096 characters overall, 100 IDs, and 64 characters per ID | +| `cweId` | 32 characters | +| `sourceIdentifier` | 512 characters | +| `cpeName` | 2,048 characters | +| `virtualMatchString` | 2,048 characters | +| `versionStart`, `versionEnd` | 256 characters each | +| CVSS metric fields | 256 characters, 32 slash-delimited tokens, and 32 characters per token | + +Requests that exceed a limit return HTTP 400 before contacting OpenSearch. These bounds limit input size and query complexity without changing NVD-style CPE matching semantics. + Use `cveIds` to request up to 100 CVE records by comma-separated CVE ID. ```bash diff --git a/api-docs/openapi.json b/api-docs/openapi.json index cbf91eb..660fd07 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -267,8 +267,9 @@ "properties": { "keywordSearch": { "type": "string", + "maxLength": 512, "example": "remote code execution", - "description": "Returns CVE records where a word or phrase is included in the record's description. Multiple keywords operate like an AND statement, returning records whose descriptions include every keyword." + "description": "Returns CVE records where a word or phrase is included in the record's description. Multiple keywords operate like an AND statement, returning records whose descriptions include every keyword. Limited to 512 characters and 64 whitespace-delimited terms." }, "keywordExactMatch": { "type": "boolean", @@ -277,8 +278,9 @@ }, "cveIds": { "type": "string", + "maxLength": 4096, "example": "CVE-2024-1234,CVE-2024-5678", - "description": "Returns one or more CVE records identified by a comma-separated list of CVE IDs. Limited to 100 CVE IDs." + "description": "Returns one or more CVE records identified by a comma-separated list of CVE IDs. Limited to 100 CVE IDs, 64 characters per ID, and 4096 characters overall." }, "noRejected": { "type": "boolean", @@ -287,6 +289,7 @@ }, "cweId": { "type": "string", + "maxLength": 32, "example": "CWE-79", "description": "Returns CVE records that include the provided Common Weakness Enumeration ID." }, @@ -371,11 +374,13 @@ }, "sourceIdentifier": { "type": "string", + "maxLength": 512, "example": "security@example.org", "description": "Returns CVE records that contain the provided source identifier." }, "cpeName": { "type": "string", + "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, @@ -425,6 +430,7 @@ }, "versionStart": { "type": "string", + "maxLength": 256, "example": "2.2", "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." }, @@ -438,6 +444,7 @@ }, "versionEnd": { "type": "string", + "maxLength": 256, "example": "2.6", "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." }, @@ -451,6 +458,7 @@ }, "virtualMatchString": { "type": "string", + "maxLength": 2048, "example": "cpe:2.3:o:microsoft:windows_10_1809", "description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component." } diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 713c042..a6518ac 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -60,6 +60,13 @@ describe('OpenAPI contract', () => { expect(schema.properties.cveIds.type).toBe('string'); expect(schema.properties.cveIds.description).toContain('comma-separated list of CVE IDs'); expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_IDS} CVE IDs`); + expect(schema.properties.cveIds.description).toContain(`${constants.MAX_CVE_ID_LENGTH} characters per ID`); + expect(schema.properties.keywordSearch.description).toContain( + `${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms` + ); + Object.entries(constants.SEARCH_STRING_LENGTH_LIMITS).forEach(([field, maxLength]) => { + expect(schema.properties[field].maxLength).toBe(maxLength); + }); expect(schema.properties.noRejected.type).toBe('boolean'); expect(schema.properties.noRejected.description).toContain('When true'); expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry'); diff --git a/controllers/middleware.js b/controllers/middleware.js index 4a3ea66..351e7af 100644 --- a/controllers/middleware.js +++ b/controllers/middleware.js @@ -90,6 +90,10 @@ function hasInvalidCvssMetricsShape(value) { ); } +function countWhitespaceDelimitedTokens(value) { + return value.trim().split(/\s+/).length; +} + function parseCveIds(value) { if (typeof value !== 'string') return null; @@ -142,11 +146,29 @@ function validateParams(req, res, next) { } } + for (const [param, maxLength] of Object.entries(constants.SEARCH_STRING_LENGTH_LIMITS)) { + if (typeof params[param] === 'string' && params[param].length > maxLength) { + return res.status(400).json(error.searchFieldLengthError(param, maxLength)); + } + } + + if ( + typeof params.keywordSearch === 'string' && + countWhitespaceDelimitedTokens(params.keywordSearch) > constants.MAX_KEYWORD_SEARCH_TOKENS + ) { + return res + .status(400) + .json(error.searchFieldTokenLimitError('keywordSearch', constants.MAX_KEYWORD_SEARCH_TOKENS)); + } + if (hasParam(params, 'cveIds')) { const cveIds = parseCveIds(params.cveIds); if (!cveIds) { return res.status(400).json(error.cveIdsFormatError()); } + if (cveIds.some((cveId) => cveId.length > constants.MAX_CVE_ID_LENGTH)) { + return res.status(400).json(error.cveIdLengthError()); + } if (cveIds.length > constants.MAX_CVE_IDS) { return res.status(400).json(error.cveIdsLimitError()); } diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index 0a83745..b036f2f 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -1,4 +1,5 @@ const mw = require('./middleware.js'); +const constants = require('../utils/constants'); describe('middleware validation function', () => { let req, res, next, status, json; @@ -30,6 +31,10 @@ describe('middleware validation function', () => { expect(next.mock.calls[0]).toHaveLength(0); } + function padBetween(prefix, suffix, length, character = 'a') { + return `${prefix}${character.repeat(length - prefix.length - suffix.length)}${suffix}`; + } + test('ValidateParams returns an error when keywordExactMatch is true without keywordSearch', () => { validate({ keywordExactMatch: true @@ -413,6 +418,108 @@ describe('middleware validation function', () => { expectValidationError('CVE_IDS_LIMIT', 'The cveIds parameter cannot contain more than 100 CVE IDs.'); }); + test.each(Object.entries(constants.SEARCH_STRING_LENGTH_LIMITS))( + 'ValidateParams returns an error when %s exceeds its maximum length.', + (param, maxLength) => { + validate({ + [param]: 'a'.repeat(maxLength + 1) + }); + + expectValidationError( + 'SEARCH_FIELD_LENGTH', + `The ${param} field must be ${maxLength} characters or fewer.` + ); + } + ); + + test.each([ + ['keywordSearch', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch)], + ['cweId', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.cweId)], + ['sourceIdentifier', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.sourceIdentifier)] + ])('ValidateParams accepts %s at its maximum length.', (param, value) => { + validate({ + [param]: value + }); + + expectValidationSuccess(); + }); + + test('ValidateParams accepts cveIds and each CVE ID at their maximum lengths.', () => { + const idPrefix = 'CVE-2024-'; + const longId = `${idPrefix}${'1'.repeat(constants.MAX_CVE_ID_LENGTH - idPrefix.length)}`; + const shortId = `${idPrefix}${'2'.repeat(32 - idPrefix.length)}`; + const mediumId = `${idPrefix}${'3'.repeat(33 - idPrefix.length)}`; + const cveIds = [...Array(62).fill(longId), shortId, mediumId].join(','); + + expect(cveIds).toHaveLength(constants.SEARCH_STRING_LENGTH_LIMITS.cveIds); + validate({ cveIds }); + + expectValidationSuccess(); + }); + + test('ValidateParams returns an error when an individual CVE ID exceeds its maximum length.', () => { + const idPrefix = 'CVE-2024-'; + + validate({ + cveIds: `${idPrefix}${'1'.repeat(constants.MAX_CVE_ID_LENGTH - idPrefix.length + 1)}` + }); + + expectValidationError( + 'CVE_ID_LENGTH', + `Each CVE ID in the cveIds field must be ${constants.MAX_CVE_ID_LENGTH} characters or fewer.` + ); + }); + + test.each([ + [ + 'cpeName', + padBetween('cpe:2.3:a:', ':product:1.0:*:*:*:*:*:*:*', constants.SEARCH_STRING_LENGTH_LIMITS.cpeName) + ], + [ + 'virtualMatchString', + padBetween('cpe:2.3:a:', ':product', constants.SEARCH_STRING_LENGTH_LIMITS.virtualMatchString) + ] + ])('ValidateParams accepts %s at its maximum length.', (param, value) => { + validate({ + [param]: value + }); + + expectValidationSuccess(); + }); + + test.each(['versionStart', 'versionEnd'])('ValidateParams accepts %s at its maximum length.', (versionParam) => { + const typeParam = `${versionParam}Type`; + + validate({ + virtualMatchString: 'cpe:2.3:a:vendor:product', + [versionParam]: '1'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS[versionParam]), + [typeParam]: 'including' + }); + + expectValidationSuccess(); + }); + + test('ValidateParams accepts keywordSearch at the maximum term count.', () => { + validate({ + keywordSearch: Array(constants.MAX_KEYWORD_SEARCH_TOKENS).fill('a').join(' ') + }); + + expectValidationSuccess(); + }); + + test('ValidateParams returns an error when keywordSearch exceeds the maximum term count.', () => { + validate({ + keywordSearch: Array(constants.MAX_KEYWORD_SEARCH_TOKENS + 1) + .fill('a') + .join(' ') + }); + + expectValidationError( + 'SEARCH_FIELD_TOKEN_LIMIT', + `The keywordSearch field must contain ${constants.MAX_KEYWORD_SEARCH_TOKENS} or fewer terms.` + ); + }); + test.each([ ['true string', 'true'], ['false string', 'false'], diff --git a/routes/search.integration.test.js b/routes/search.integration.test.js index e153c61..e2fe32e 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -1,4 +1,5 @@ const { request } = require('../test/http'); +const constants = require('../utils/constants'); const mockApiSearch = jest.fn(); const mockBasicSearch = jest.fn(); @@ -106,6 +107,25 @@ describe('search route integration', () => { expect(mockApiSearch).not.toHaveBeenCalled(); }); + test('POST /search rejects excessive keyword terms before invoking OpenSearch', async () => { + const response = await request(createApp(), { + method: 'POST', + path: '/search', + body: { + keywordSearch: Array(constants.MAX_KEYWORD_SEARCH_TOKENS + 1) + .fill('term') + .join(' ') + } + }); + + expect(response.status).toBe(400); + expect(response.body).toStrictEqual({ + error: 'SEARCH_FIELD_TOKEN_LIMIT', + message: `The keywordSearch field must contain ${constants.MAX_KEYWORD_SEARCH_TOKENS} or fewer terms.` + }); + expect(mockApiSearch).not.toHaveBeenCalled(); + }); + test('POST /search rejects an inverted virtualMatchString version range before invoking OpenSearch', async () => { const response = await request(createApp(), { method: 'POST', diff --git a/routes/swagger.js b/routes/swagger.js index 76e8c2e..5a78b14 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -15,9 +15,10 @@ function searchRequestProperty(description, schema) { const searchRequestProperties = { keywordSearch: searchRequestProperty( - "Returns CVE records where a word or phrase is included in the record's description. Multiple keywords operate like an AND statement, returning records whose descriptions include every keyword.", + `Returns CVE records where a word or phrase is included in the record's description. Multiple keywords operate like an AND statement, returning records whose descriptions include every keyword. Limited to ${constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch} characters and ${constants.MAX_KEYWORD_SEARCH_TOKENS} whitespace-delimited terms.`, { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch, example: 'remote code execution' } ), @@ -29,9 +30,10 @@ const searchRequestProperties = { } ), cveIds: searchRequestProperty( - `Returns one or more CVE records identified by a comma-separated list of CVE IDs. Limited to ${constants.MAX_CVE_IDS} CVE IDs.`, + `Returns one or more CVE records identified by a comma-separated list of CVE IDs. Limited to ${constants.MAX_CVE_IDS} CVE IDs, ${constants.MAX_CVE_ID_LENGTH} characters per ID, and ${constants.SEARCH_STRING_LENGTH_LIMITS.cveIds} characters overall.`, { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cveIds, example: 'CVE-2024-1234,CVE-2024-5678' } ), @@ -41,6 +43,7 @@ const searchRequestProperties = { }), cweId: searchRequestProperty('Returns CVE records that include the provided Common Weakness Enumeration ID.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cweId, example: 'CWE-79' }), cvssV2Severity: searchRequestProperty( @@ -120,12 +123,14 @@ const searchRequestProperties = { ), sourceIdentifier: searchRequestProperty('Returns CVE records that contain the provided source identifier.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.sourceIdentifier, example: 'security@example.org' }), cpeName: searchRequestProperty( 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, example: 'cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*' } ), @@ -191,6 +196,7 @@ const searchRequestProperties = { 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionStart, example: '2.2' } ), @@ -205,6 +211,7 @@ const searchRequestProperties = { 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionEnd, example: '2.6' } ), @@ -219,6 +226,7 @@ const searchRequestProperties = { 'Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component.', { type: 'string', + maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.virtualMatchString, example: 'cpe:2.3:o:microsoft:windows_10_1809' } ) diff --git a/utils/constants.js b/utils/constants.js index 6a98784..e282a28 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -5,6 +5,18 @@ const MAX_CVSS_METRICS_LENGTH = 256; const MAX_CVSS_METRIC_TOKENS = 32; const MAX_CVSS_METRIC_TOKEN_LENGTH = 32; const MAX_CVE_IDS = 100; +const MAX_CVE_ID_LENGTH = 64; +const MAX_KEYWORD_SEARCH_TOKENS = 64; +const SEARCH_STRING_LENGTH_LIMITS = Object.freeze({ + keywordSearch: 512, + cveIds: 4096, + cweId: 32, + sourceIdentifier: 512, + cpeName: 2048, + versionStart: 256, + versionEnd: 256, + virtualMatchString: 2048 +}); const SEARCH_POST_FILTER_CACHE_TTL_MS = 60 * 1000; const SEARCH_POST_FILTER_CACHE_MAX_ENTRIES = 100; const SEARCH_POST_FILTER_COLD_SEARCH_CONCURRENCY = 10; @@ -125,6 +137,9 @@ module.exports = { MAX_CVSS_METRIC_TOKENS, MAX_CVSS_METRIC_TOKEN_LENGTH, MAX_CVE_IDS, + MAX_CVE_ID_LENGTH, + MAX_KEYWORD_SEARCH_TOKENS, + SEARCH_STRING_LENGTH_LIMITS, SEARCH_POST_FILTER_CACHE_TTL_MS, SEARCH_POST_FILTER_CACHE_MAX_ENTRIES, SEARCH_POST_FILTER_COLD_SEARCH_CONCURRENCY, diff --git a/utils/errors.js b/utils/errors.js index 73b5a8a..d0ecd06 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -150,6 +150,21 @@ class errors { return makeError('SEARCH_FIELD_STRING', `The ${field} field must be a non-empty string.`); } + searchFieldLengthError(field, maxLength) { + return makeError('SEARCH_FIELD_LENGTH', `The ${field} field must be ${maxLength} characters or fewer.`); + } + + searchFieldTokenLimitError(field, maxTokens) { + return makeError('SEARCH_FIELD_TOKEN_LIMIT', `The ${field} field must contain ${maxTokens} or fewer terms.`); + } + + cveIdLengthError() { + return makeError( + 'CVE_ID_LENGTH', + `Each CVE ID in the cveIds field must be ${constants.MAX_CVE_ID_LENGTH} characters or fewer.` + ); + } + cvssV2SeverityError(severity) { return makeError( 'INVALID_CVSSV2_SEVERITY', From efbc94ef2163e283284065e02ea7bead7d5e651a Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 09:42:47 -0400 Subject: [PATCH 11/20] fix: cancel abandoned search work --- README.md | 14 +- controllers/searchController.js | 189 +++++++------- controllers/searchController.test.js | 317 +++++++++++++++++------- routes/search.integration.test.js | 52 ++-- utils/searchDeadline.js | 89 ++++++- utils/searchDeadline.test.js | 95 ++++++- utils/searchInFlightRegistry.js | 118 +++++++++ utils/searchInFlightRegistry.test.js | 133 ++++++++++ utils/searchPerformance.js | 3 + utils/searchPerformance.test.js | 4 + utils/searchPostFilterCache.js | 34 +-- utils/searchPostFilterCache.test.js | 44 ---- utils/searchPostFilterWorkQueue.js | 34 ++- utils/searchPostFilterWorkQueue.test.js | 45 ++++ utils/searchProvider.js | 113 +++++++-- utils/searchProvider.test.js | 156 +++++++++--- utils/searchQueryBuilder.test.js | 35 +++ 17 files changed, 1110 insertions(+), 365 deletions(-) create mode 100644 utils/searchInFlightRegistry.js create mode 100644 utils/searchInFlightRegistry.test.js diff --git a/README.md b/README.md index 65ccf31..9903dd3 100644 --- a/README.md +++ b/README.md @@ -233,9 +233,11 @@ Every request receives an `X-Request-ID` response header. A caller-provided `X-R Access logs omit query strings and request bodies. By default, the API retains 10% of successful non-health-check access logs, suppresses successful `/api/health-check` access logs, and retains every HTTP 4xx or 5xx access log. Set `RequestLogSuccessSampleRate` from `0` through `1` to change successful-request sampling. Set `RequestLogHealthChecks=true` to retain successful health-check logs. -The API always emits a sanitized JSON `search.performance` event for `/search` queue saturation, queue wait timeout, search timeout, OpenSearch connection errors, and upstream search errors. These events include the request ID, outcome, HTTP status, strategy, cache state, total elapsed time, and queue state when applicable. Search terms, CPE values, request bodies, credentials, and upstream error details are never included. +The API always emits a sanitized JSON `search.performance` event for `/search` queue saturation, queue wait timeout, search timeout, client disconnect, OpenSearch connection errors, and upstream search errors. These events include the request ID, outcome, HTTP status, strategy, cache state, total elapsed time, cancellation reason, and queue state when applicable. Search terms, CPE values, request bodies, credentials, and upstream error details are never included. -Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. Successful requests then emit the same structured event with additional redacted timing details, including candidate counts, filtered counts, returned count, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. +Set `SearchLogPerformanceTimings=true` on the API deployment when diagnosing slow `/search` requests. Successful requests then emit the same structured event with additional redacted timing details, including candidate counts, filtered counts, returned count, OpenSearch operation types, maximum observed active OpenSearch requests in the process, OpenSearch elapsed time, OpenSearch `took`, post-filter elapsed time, cold-search queue wait time, page fetch elapsed or wait time, and total handler elapsed time. Leave it disabled during normal operation unless the deployment logging volume is acceptable. + +Each `/search` request owns a lifecycle cancellation signal. When its overall deadline expires, the API aborts the active OpenSearch transport request before returning HTTP 504. When the caller disconnects, the API aborts work that is needed only by that caller and does not attempt to write a response. This prevents timed-out or abandoned standard searches from continuing until the transport timeout. ### Application Rate Limiting @@ -257,7 +259,7 @@ The cache is intentionally small and short-lived: - Max entries: 100 - Scope: one Node.js process - Stored value: filtered CVE IDs and candidate count -- In-flight behavior: concurrent identical cold searches share one candidate query per process +- In-flight behavior: concurrent identical cold searches share one reference-counted candidate query per process - Cold-search concurrency: cold CPE candidate queries are limited to 10 per process before reaching OpenSearch - Cold-search queue depth: up to 500 unique cold searches can wait per process behind the concurrency limit - Cold-search queue wait: up to 10 seconds before a waiting search returns HTTP 429 @@ -267,7 +269,7 @@ The cache is intentionally small and short-lived: When the cold-search queue is full, new unique CPE post-filter searches fail closed with HTTP 429 and a `SEARCH_QUEUE_FULL` response. Identical requests for a search that is already in flight still wait on the shared in-flight result and do not consume additional queue slots. -When a cold search waits in the queue for more than 10 seconds, it fails closed with HTTP 429 and a `SEARCH_QUEUE_TIMEOUT` response. OpenSearch operations use one attempt and return a sanitized HTTP 504 `SEARCH_TIMEOUT` response when they exceed 20 seconds. The same response is returned when a `/search` request exceeds the 30-second overall processing deadline. +When a cold search waits in the queue for more than 10 seconds, it fails closed with HTTP 429 and a `SEARCH_QUEUE_TIMEOUT` response. OpenSearch operations use one attempt and return a sanitized HTTP 504 `SEARCH_TIMEOUT` response when they exceed 20 seconds. The same response is returned when a `/search` request exceeds the 30-second overall processing deadline, and the active OpenSearch request is aborted. Healthy dev timing logs showed the impact for repeated identical `virtualMatchString` range searches: @@ -282,9 +284,9 @@ Healthy dev timing logs showed the impact for repeated identical `virtualMatchSt | Avg candidates scanned | 10000 | cached from prior result | | Avg filtered CVEs | 4 | 4 | -This helps most when multiple users submit the same expensive CPE search within the TTL. The first request is a cold cache miss; later identical requests on the same running task are warm cache hits. If identical cold requests arrive concurrently before the first one finishes, the later requests wait on the same candidate query instead of starting duplicate 10,000-candidate OpenSearch requests. Each request still fetches its requested page of full CVE records after the shared candidate result is available. +This helps most when multiple users submit the same expensive CPE search within the TTL. The first request is a cold cache miss; later identical requests on the same running task are warm cache hits. If identical cold requests arrive concurrently before the first one finishes, the later requests wait on the same candidate query instead of starting duplicate 10,000-candidate OpenSearch requests. Disconnecting one waiter does not cancel work still needed by another waiter; the shared operation is aborted when its final waiter disconnects or times out. Each active request still fetches its requested page of full CVE records after the shared candidate result is available. -The cache does not share entries across ECS tasks. With multiple tasks, each task warms independently depending on load balancer routing. Steady repeated traffic can warm every active task, but unique one-off searches still pay the cold-cache cost. The per-process TTL, entry limit, cold-search concurrency, queue depth, queue wait, OpenSearch timeout, and overall request deadline can be tuned with `SearchPostFilterCacheTtlMs`, `SearchPostFilterCacheMaxEntries`, `SearchPostFilterColdSearchConcurrency`, `SearchPostFilterColdSearchMaxQueueDepth`, `SearchPostFilterColdSearchMaxWaitMs`, `SearchOpenSearchRequestTimeoutMs`, and `SearchRequestDeadlineMs`. Future scale improvements could add a shared cache such as Redis/ElastiCache. +The cache does not share entries across ECS tasks. With multiple tasks, each task warms independently depending on load balancer routing. Steady repeated traffic can warm every active task, but unique one-off searches still pay the cold-cache cost. The request lifecycle and in-flight waiter registry are separate from this result cache, so the cache can later be replaced with Redis or ElastiCache without changing cancellation behavior. The per-process TTL, entry limit, cold-search concurrency, queue depth, queue wait, OpenSearch timeout, and overall request deadline can be tuned with `SearchPostFilterCacheTtlMs`, `SearchPostFilterCacheMaxEntries`, `SearchPostFilterColdSearchConcurrency`, `SearchPostFilterColdSearchMaxQueueDepth`, `SearchPostFilterColdSearchMaxWaitMs`, `SearchOpenSearchRequestTimeoutMs`, and `SearchRequestDeadlineMs`. Docker runtime smoke checks can be run after building the image. The default `Dockerfile` avoids BuildKit `RUN --mount` syntax for AWS builders that do not support it: diff --git a/controllers/searchController.js b/controllers/searchController.js index 3ec5c52..a76564b 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -4,6 +4,7 @@ const cpe = require('../utils/cpe'); const searchPerformance = require('../utils/searchPerformance'); const searchDeadline = require('../utils/searchDeadline'); const searchPostFilterCache = require('../utils/searchPostFilterCache'); +const searchInFlightRegistry = require('../utils/searchInFlightRegistry'); const searchPostFilterWorkQueue = require('../utils/searchPostFilterWorkQueue'); const searchProvider = require('../utils/searchProvider'); const searchQueryBuilder = require('../utils/searchQueryBuilder'); @@ -172,20 +173,32 @@ function combineTook(...values) { return numericValues.reduce((sum, value) => sum + value, 0); } -async function executeStandardSearch( - searchManager, - searchOptions, - queryStringObjects, - postFilters, - responseOptions, - metrics -) { +function recordOpenSearchStart(metrics, operation, state) { + metrics.openSearchOperations ??= []; + metrics.openSearchOperations.push(operation); + metrics.maxOpenSearchActiveRequests = Math.max(metrics.maxOpenSearchActiveRequests ?? 0, state.activeCount); +} + +async function executeOpenSearch(body, operation, signal, metrics) { + return searchProvider.searchOpenSearch(body, { + signal, + operation, + onStart: (state) => recordOpenSearchStart(metrics, operation, state) + }); +} + +async function executeStandardSearch(searchOptions, queryStringObjects, postFilters, responseOptions, metrics, signal) { metrics.strategy = 'standard'; const searchStart = searchPerformance.markTime(); - const searchResp = await searchManager.apiSearch(null, searchOptions, queryStringObjects); + const body = searchQueryBuilder.buildOpenSearchBody(searchOptions, queryStringObjects); + const searchData = await executeOpenSearch(body, 'standard', signal, metrics); + const searchResp = { + status: 'ok', + data: searchData + }; metrics.openSearchElapsedMs = searchPerformance.elapsedMs(searchStart); - metrics.openSearchTookMs = searchResp?.data?.took; + metrics.openSearchTookMs = searchData?.took; const postFilterStart = searchPerformance.markTime(); const resp = applySearchPostFilters(searchResp, postFilters, responseOptions, metrics); @@ -194,39 +207,38 @@ async function executeStandardSearch( return resp; } -async function fetchFullHitsByCveIds(searchManager, cveIds, metrics) { +async function fetchFullHitsByCveIds(cveIds, metrics, signal) { if (!cveIds.length) return []; - const cacheKey = searchPostFilterCache.buildInFlightKey('pageFetch', cveIds); - const inFlightResult = searchPostFilterCache.getInFlight(cacheKey); - if (inFlightResult) { - metrics.pageFetchInFlightHit = true; + const inFlightKey = searchInFlightRegistry.buildKey('pageFetch', cveIds); + const waitStart = searchPerformance.markTime(); + const inFlight = searchInFlightRegistry.acquire( + inFlightKey, + async (operationSignal) => { + const { searchOptions, queryStringObjects } = searchQueryBuilder.buildCveIdsSearchRequest(cveIds); + const body = searchQueryBuilder.buildOpenSearchBody(searchOptions, queryStringObjects); + const pageFetchStart = searchPerformance.markTime(); + const pageData = await executeOpenSearch(body, 'page-fetch', operationSignal, metrics); + metrics.pageFetchElapsedMs = searchPerformance.elapsedMs(pageFetchStart); + metrics.pageFetchTookMs = pageData?.took; + + return orderHitsByCveIds(pageData?.hits?.hits ?? [], cveIds); + }, + { signal } + ); - const waitStart = searchPerformance.markTime(); - const result = await inFlightResult; + metrics.pageFetchInFlightHit = inFlight.shared; + const result = await inFlight.promise; + if (inFlight.shared) { metrics.pageFetchWaitElapsedMs = searchPerformance.elapsedMs(waitStart); - - return result; } - metrics.pageFetchInFlightHit = false; - - const { searchOptions, queryStringObjects } = searchQueryBuilder.buildCveIdsSearchRequest(cveIds); - const promise = (async () => { - const pageFetchStart = searchPerformance.markTime(); - const pageResp = await searchManager.apiSearch(null, searchOptions, queryStringObjects); - metrics.pageFetchElapsedMs = searchPerformance.elapsedMs(pageFetchStart); - metrics.pageFetchTookMs = pageResp?.data?.took; - - return orderHitsByCveIds(pageResp?.data?.hits?.hits ?? [], cveIds); - })(); - - return searchPostFilterCache.setInFlight(cacheKey, promise); + return result; } -async function buildPostFilterCandidateResult(candidateBody, postFilters, metrics) { +async function buildPostFilterCandidateResult(candidateBody, postFilters, metrics, signal) { const candidateStart = searchPerformance.markTime(); - const candidateData = await searchProvider.searchRaw(candidateBody); + const candidateData = await executeOpenSearch(candidateBody, 'cpe-candidate', signal, metrics); metrics.openSearchElapsedMs = searchPerformance.elapsedMs(candidateStart); metrics.openSearchTookMs = candidateData?.took; @@ -244,7 +256,7 @@ async function buildPostFilterCandidateResult(candidateBody, postFilters, metric }; } -async function getPostFilterCandidateResult(cacheKey, candidateBody, postFilters, metrics) { +async function getPostFilterCandidateResult(cacheKey, candidateBody, postFilters, metrics, signal) { const cachedResult = searchPostFilterCache.get(cacheKey); if (cachedResult) { metrics.cacheHit = true; @@ -252,55 +264,58 @@ async function getPostFilterCandidateResult(cacheKey, candidateBody, postFilters return cachedResult; } - const inFlightResult = searchPostFilterCache.getInFlight(cacheKey); - if (inFlightResult) { - metrics.cacheHit = false; - metrics.inFlightHit = true; + metrics.cacheHit = false; + const inFlightKey = searchInFlightRegistry.buildKey('candidate', cacheKey); + const queueStart = searchPerformance.markTime(); + const waitStart = searchPerformance.markTime(); + const inFlight = searchInFlightRegistry.acquire( + inFlightKey, + (operationSignal) => + searchPostFilterWorkQueue + .run( + () => { + metrics.coldSearchQueueWaitElapsedMs = searchPerformance.elapsedMs(queueStart); + metrics.coldSearchQueueState = searchPostFilterWorkQueue.getState(); + return buildPostFilterCandidateResult(candidateBody, postFilters, metrics, operationSignal); + }, + { signal: operationSignal } + ) + .then((result) => { + searchPostFilterCache.set(cacheKey, result); + return result; + }), + { signal } + ); - const waitStart = searchPerformance.markTime(); - const result = await inFlightResult; + metrics.inFlightHit = inFlight.shared; + const result = await inFlight.promise; + if (inFlight.shared) { metrics.inFlightWaitElapsedMs = searchPerformance.elapsedMs(waitStart); - - return result; } - metrics.cacheHit = false; - metrics.inFlightHit = false; - - const queueStart = searchPerformance.markTime(); - const promise = searchPostFilterWorkQueue - .run(() => { - metrics.coldSearchQueueWaitElapsedMs = searchPerformance.elapsedMs(queueStart); - metrics.coldSearchQueueState = searchPostFilterWorkQueue.getState(); - return buildPostFilterCandidateResult(candidateBody, postFilters, metrics); - }) - .then((result) => { - searchPostFilterCache.set(cacheKey, result); - return result; - }); - - return searchPostFilterCache.setInFlight(cacheKey, promise); + return result; } async function executeOptimizedPostFilterSearch( - searchManager, searchOptions, queryStringObjects, postFilters, responseOptions, - metrics + metrics, + signal ) { metrics.strategy = 'cpe-candidate'; const candidateBody = searchQueryBuilder.buildCpeCandidateSearchBody(searchOptions, queryStringObjects); const cacheKey = searchPostFilterCache.buildCacheKey(candidateBody, postFilters); - const candidateResult = await getPostFilterCandidateResult(cacheKey, candidateBody, postFilters, metrics); + const candidateResult = await getPostFilterCandidateResult(cacheKey, candidateBody, postFilters, metrics, signal); + searchDeadline.throwIfAborted(signal); const pageCveIds = paginateHits(candidateResult.filteredCveIds, responseOptions); metrics.candidateCount = candidateResult.candidateCount; metrics.filteredCount = candidateResult.filteredCount ?? candidateResult.filteredCveIds.length; metrics.returnedCount = pageCveIds.length; - const fullPageHits = await fetchFullHitsByCveIds(searchManager, pageCveIds, metrics); + const fullPageHits = await fetchFullHitsByCveIds(pageCveIds, metrics, signal); const response = { data: { took: combineTook(metrics.openSearchTookMs, metrics.pageFetchTookMs), @@ -316,25 +331,18 @@ async function executeOptimizedPostFilterSearch( return buildPagedSearchResponse(response, metrics.filteredCount, fullPageHits); } -async function executeSearch(searchManager, searchOptions, queryStringObjects, postFilters, responseOptions, metrics) { +async function executeSearch(searchOptions, queryStringObjects, postFilters, responseOptions, metrics, signal) { if (!hasSearchPostFilters(postFilters)) { - return executeStandardSearch( - searchManager, - searchOptions, - queryStringObjects, - postFilters, - responseOptions, - metrics - ); + return executeStandardSearch(searchOptions, queryStringObjects, postFilters, responseOptions, metrics, signal); } return executeOptimizedPostFilterSearch( - searchManager, searchOptions, queryStringObjects, postFilters, responseOptions, - metrics + metrics, + signal ); } @@ -344,28 +352,30 @@ async function searchHandler(req, res, next) { requestId: req.requestId }; const handlerStart = searchPerformance.markTime(); + const lifecycle = searchDeadline.createSearchRequestLifecycle( + req, + res, + searchProvider.getSearchRequestDeadlineMs() + ); let searchOptions; let postFilters; let responseOptions; try { - const searchManager = searchProvider.getAdvancedSearchManager(); const searchRequest = searchQueryBuilder.buildSearchRequest(params); searchOptions = searchRequest.searchOptions; postFilters = searchRequest.postFilters; responseOptions = searchRequest.responseOptions; - const resp = await searchDeadline.withSearchDeadline( - executeSearch( - searchManager, - searchOptions, - searchRequest.queryStringObjects, - postFilters, - responseOptions, - metrics - ), - searchProvider.getSearchRequestDeadlineMs() + const resp = await executeSearch( + searchOptions, + searchRequest.queryStringObjects, + postFilters, + responseOptions, + metrics, + lifecycle.signal ); + lifecycle.throwIfAborted(); const pagingOptions = responseOptions ?? searchOptions; metrics.outcome = 'success'; metrics.statusCode = 200; @@ -384,6 +394,13 @@ async function searchHandler(req, res, next) { ...resp }); } catch (err) { + if (searchDeadline.isSearchClientDisconnectedError(err)) { + metrics.outcome = 'client_disconnected'; + metrics.statusCode = 499; + metrics.abortReason = 'client_disconnect'; + return; + } + if (searchPostFilterWorkQueue.isQueueFullError(err)) { metrics.outcome = 'queue_full'; metrics.statusCode = 429; @@ -407,6 +424,7 @@ async function searchHandler(req, res, next) { metrics.outcome = 'search_timeout'; metrics.statusCode = 504; metrics.timeoutSource = err?.name === 'TimeoutError' ? 'opensearch' : 'request_deadline'; + metrics.abortReason = metrics.timeoutSource; return res.status(504).json(error.searchTimeoutError()); } @@ -422,6 +440,7 @@ async function searchHandler(req, res, next) { metrics.statusCode = 502; return res.status(502).json(error.upstreamSearchError()); } finally { + lifecycle.complete(); metrics.totalElapsedMs = searchPerformance.elapsedMs(handlerStart); searchPerformance.logSearchPerformance(metrics, searchOptions, responseOptions, postFilters); } diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 750f0d4..387703b 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -1,6 +1,20 @@ +const { EventEmitter } = require('events'); + const mockApiSearch = jest.fn(); const mockBasicSearch = jest.fn(); const mockSearchRaw = jest.fn(); +const mockSearchOpenSearch = jest.fn((body, options) => { + const operation = options?.operation; + const search = operation === 'cpe-candidate' ? mockSearchRaw : mockApiSearch; + options?.onStart?.({ + activeCount: 1, + activeByOperation: { + [operation]: 1 + } + }); + + return Promise.resolve(search(body, options)).then((result) => result?.data ?? result); +}); const mockShouldLogSearchPerformance = jest.fn(); const mockGetPostFilterCacheTtlMs = jest.fn(() => 60000); const mockGetPostFilterCacheMaxEntries = jest.fn(() => 100); @@ -8,9 +22,6 @@ const mockGetPostFilterColdSearchConcurrency = jest.fn(() => 10); const mockGetPostFilterColdSearchMaxQueueDepth = jest.fn(() => 500); const mockGetPostFilterColdSearchMaxWaitMs = jest.fn(() => 10000); const mockGetSearchRequestDeadlineMs = jest.fn(() => 30000); -const mockGetAdvancedSearchManager = jest.fn(() => ({ - apiSearch: mockApiSearch -})); const mockGetBasicSearchManager = jest.fn(() => ({ search: mockBasicSearch })); @@ -24,7 +35,6 @@ jest.mock('cve-core', () => { }); jest.mock('../utils/searchProvider', () => ({ - getAdvancedSearchManager: mockGetAdvancedSearchManager, getBasicSearchManager: mockGetBasicSearchManager, getPostFilterCacheMaxEntries: mockGetPostFilterCacheMaxEntries, getPostFilterCacheTtlMs: mockGetPostFilterCacheTtlMs, @@ -32,11 +42,12 @@ jest.mock('../utils/searchProvider', () => ({ getPostFilterColdSearchMaxQueueDepth: mockGetPostFilterColdSearchMaxQueueDepth, getPostFilterColdSearchMaxWaitMs: mockGetPostFilterColdSearchMaxWaitMs, getSearchRequestDeadlineMs: mockGetSearchRequestDeadlineMs, - searchRaw: mockSearchRaw, + searchOpenSearch: mockSearchOpenSearch, shouldLogSearchPerformance: mockShouldLogSearchPerformance })); const controller = require('./searchController.js'); +const searchInFlightRegistry = require('../utils/searchInFlightRegistry'); const searchPostFilterCache = require('../utils/searchPostFilterCache'); function createResponse() { @@ -49,6 +60,7 @@ function createResponse() { function advancedResponse(totalValue, hits = []) { return { + status: 'ok', data: { hits: { total: { @@ -134,9 +146,9 @@ describe('Search controller SEARCH handler', () => { mockApiSearch.mockReset(); mockBasicSearch.mockReset(); mockSearchRaw.mockReset(); + mockSearchOpenSearch.mockClear(); mockShouldLogSearchPerformance.mockReset(); mockShouldLogSearchPerformance.mockReturnValue(false); - mockGetAdvancedSearchManager.mockClear(); mockGetBasicSearchManager.mockClear(); mockGetPostFilterCacheTtlMs.mockClear(); mockGetPostFilterCacheMaxEntries.mockClear(); @@ -151,6 +163,7 @@ describe('Search controller SEARCH handler', () => { mockGetPostFilterColdSearchMaxWaitMs.mockReturnValue(10000); mockGetSearchRequestDeadlineMs.mockReturnValue(30000); searchPostFilterCache.clear(); + searchInFlightRegistry.clear(); jest.spyOn(console, 'log').mockImplementation(() => {}); res = createResponse(); @@ -158,6 +171,7 @@ describe('Search controller SEARCH handler', () => { }); afterEach(() => { + searchInFlightRegistry.clear(); jest.restoreAllMocks(); }); @@ -182,48 +196,53 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockGetAdvancedSearchManager).toHaveBeenCalledTimes(1); - expect(mockApiSearch).toHaveBeenCalledWith( - null, { - filters: [ - { - terms: { - 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] - } - } - ], - rangeObjects: [ - { - range: { - 'cveMetadata.datePublished': { - gte: '2024-01-01T00:00:00.000Z', - lte: '2024-01-31T00:00:00.000Z' + query: { + bool: { + must: [ + { + simple_query_string: { + query: 'remote* code*', + default_operator: 'AND', + fields: ['containers.cna.descriptions.value'] + } } - } - }, - { - range: { - 'cveMetadata.dateUpdated': { - gte: '2024-02-01T00:00:00.000Z', - lte: '2024-02-29T00:00:00.000Z' + ], + filter: [ + { + terms: { + 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] + } + }, + { + range: { + 'cveMetadata.datePublished': { + gte: '2024-01-01T00:00:00.000Z', + lte: '2024-01-31T00:00:00.000Z' + } + } + }, + { + range: { + 'cveMetadata.dateUpdated': { + gte: '2024-02-01T00:00:00.000Z', + lte: '2024-02-29T00:00:00.000Z' + } + } } - } + ] } - ], - resultsPerPage: 10, + }, + track_total_hits: true, + size: 10, from: 20 }, - [ - { - simple_query_string: { - query: 'remote* code*', - default_operator: 'AND', - fields: ['containers.cna.descriptions.value'] - } - } - ] + expect.objectContaining({ + operation: 'standard', + signal: expect.any(AbortSignal), + onStart: expect.any(Function) + }) ); expect(res.status).toHaveBeenCalledWith(200); @@ -249,19 +268,16 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1]).toStrictEqual({ - filters: [ + expect(mockApiSearch.mock.calls[0][0].query.bool).toStrictEqual({ + must: [], + filter: [ { match_phrase: { 'containers.cna.descriptions.value': 'remote code' } } - ], - rangeObjects: [], - resultsPerPage: 20, - from: 0 + ] }); - expect(mockApiSearch.mock.calls[0][2]).toStrictEqual([]); }); test('SEARCH builds an exact terms filter for cveIds', async () => { @@ -277,14 +293,14 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1].filters).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ { terms: { 'cveMetadata.cveId.keyword': ['CVE-2024-0001', 'CVE-2024-0002'] } } ]); - expect(mockApiSearch.mock.calls[0][2]).toStrictEqual([]); + expect(mockApiSearch.mock.calls[0][0].query.bool.must).toStrictEqual([]); }); test('SEARCH builds rejected-state exclusions for noRejected', async () => { @@ -300,7 +316,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1].filters).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ { bool: { must_not: [ @@ -322,7 +338,7 @@ describe('Search controller SEARCH handler', () => { } } ]); - expect(mockApiSearch.mock.calls[0][2]).toStrictEqual([]); + expect(mockApiSearch.mock.calls[0][0].query.bool.must).toStrictEqual([]); }); test('SEARCH processes virtualMatchString into a structured CPE criteria filter', async () => { @@ -497,20 +513,27 @@ describe('Search controller SEARCH handler', () => { ); expect(mockApiSearch).toHaveBeenCalledWith( - null, { - filters: [ - { - terms: { - 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] - } + query: { + bool: { + must: [], + filter: [ + { + terms: { + 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] + } + } + ] } - ], - rangeObjects: [], - resultsPerPage: 1, + }, + track_total_hits: true, + size: 1, from: 0 }, - [] + expect.objectContaining({ + operation: 'page-fetch', + signal: expect.any(AbortSignal) + }) ); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ @@ -570,18 +593,24 @@ describe('Search controller SEARCH handler', () => { expect(mockSearchRaw.mock.calls[0][0].size).toBe(10000); expect(mockSearchRaw.mock.calls[0][0].from).toBe(0); - expect(mockApiSearch.mock.calls[0][1]).toStrictEqual({ - filters: [ - { - terms: { - 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] - } + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ + { + terms: { + 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] } - ], - rangeObjects: [], - resultsPerPage: 1, + } + ]); + expect(mockApiSearch.mock.calls[0][0]).toMatchObject({ + track_total_hits: true, + size: 1, from: 0 }); + expect(mockApiSearch.mock.calls[0][1]).toEqual( + expect.objectContaining({ + operation: 'page-fetch', + signal: expect.any(AbortSignal) + }) + ); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith({ page: 1, @@ -635,16 +664,16 @@ describe('Search controller SEARCH handler', () => { expect(mockSearchRaw).toHaveBeenCalledTimes(1); expect(mockApiSearch).toHaveBeenCalledTimes(2); - expect(mockApiSearch.mock.calls[1][1]).toStrictEqual({ - filters: [ - { - terms: { - 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] - } + expect(mockApiSearch.mock.calls[1][0].query.bool.filter).toStrictEqual([ + { + terms: { + 'cveMetadata.cveId.keyword': ['CVE-2024-0001'] } - ], - rangeObjects: [], - resultsPerPage: 1, + } + ]); + expect(mockApiSearch.mock.calls[1][0]).toMatchObject({ + track_total_hits: true, + size: 1, from: 0 }); expect(secondRes.status).toHaveBeenCalledWith(200); @@ -724,6 +753,56 @@ describe('Search controller SEARCH handler', () => { }); }); + test('SEARCH keeps shared CPE work active when one client disconnects', async () => { + const candidateSearch = deferred(); + const applicable = cpeHit('CVE-2024-0001', { + criteria: 'cpe:2.3:o:linux:linux_kernel:2.5:*:*:*:*:*:*:*' + }); + const fullApplicable = { + _id: 'CVE-2024-0001', + _source: { + cveMetadata: { + cveId: 'CVE-2024-0001' + } + } + }; + const firstReq = new EventEmitter(); + const secondReq = new EventEmitter(); + const query = { + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + resultsPerPage: 1, + pageNumber: 1 + }; + firstReq.query = query; + secondReq.query = query; + const firstRes = createResponse(); + const secondRes = createResponse(); + let operationSignal; + + mockSearchRaw.mockImplementation((_body, { signal }) => { + operationSignal = signal; + return candidateSearch.promise; + }); + mockApiSearch.mockResolvedValue(advancedResponse(1, [fullApplicable])); + + const firstSearch = controller.SEARCH(firstReq, firstRes, next); + await Promise.resolve(); + const secondSearch = controller.SEARCH(secondReq, secondRes, next); + await Promise.resolve(); + + firstReq.emit('aborted'); + await firstSearch; + + expect(operationSignal.aborted).toBe(false); + candidateSearch.resolve(rawResponse(1, [applicable])); + await secondSearch; + + expect(mockSearchRaw).toHaveBeenCalledTimes(1); + expect(mockApiSearch).toHaveBeenCalledTimes(1); + expect(firstRes.status).not.toHaveBeenCalled(); + expect(secondRes.status).toHaveBeenCalledWith(200); + }); + test('SEARCH applies semantic virtualMatchString filtering before building response metadata', async () => { const applicable = cpeHit('CVE-2024-0001', { criteria: 'cpe:2.3:o:microsoft:windows_10_1809:-:*:*:*:*:*:x86:*' @@ -952,6 +1031,8 @@ describe('Search controller SEARCH handler', () => { optimizedFallback: false, cacheHit: false, inFlightHit: false, + openSearchOperations: ['standard'], + maxOpenSearchActiveRequests: 1, postFilters: [], requestedFrom: 0, requestedSize: 1, @@ -974,7 +1055,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1].filters).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ { term: { 'containers.cna.metrics.cvssV3_0.baseSeverity.keyword': 'HIGH' @@ -1001,7 +1082,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1].filters).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ { bool: { filter: [ @@ -1046,7 +1127,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][1].filters).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.filter).toStrictEqual([ { bool: { filter: [ @@ -1103,7 +1184,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][2]).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.must).toStrictEqual([ { simple_query_string: { query: 'c\\+\\+* path\\:\\/tmp* \\(test\\)*', @@ -1127,7 +1208,7 @@ describe('Search controller SEARCH handler', () => { next ); - expect(mockApiSearch.mock.calls[0][2]).toStrictEqual([ + expect(mockApiSearch.mock.calls[0][0].query.bool.must).toStrictEqual([ { simple_query_string: { query: '', @@ -1309,7 +1390,12 @@ describe('Search controller SEARCH handler', () => { test('SEARCH returns a sanitized 504 when the overall request deadline is exceeded', async () => { const timers = captureTimers(); - mockApiSearch.mockReturnValue(new Promise(() => {})); + mockApiSearch.mockImplementation( + (_body, { signal }) => + new Promise((resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }) + ); const search = controller.SEARCH( { @@ -1335,10 +1421,63 @@ describe('Search controller SEARCH handler', () => { requestId: 'request-deadline-request', outcome: 'search_timeout', statusCode: 504, - timeoutSource: 'request_deadline' + timeoutSource: 'request_deadline', + abortReason: 'request_deadline' }); }); + test('SEARCH stops without writing a response when the client disconnects', async () => { + const req = new EventEmitter(); + req.requestId = 'client-disconnect-request'; + req.query = {}; + mockApiSearch.mockImplementation( + (_body, { signal }) => + new Promise((resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }) + ); + + const search = controller.SEARCH(req, res, next); + await Promise.resolve(); + req.emit('aborted'); + await search; + + expect(res.status).not.toHaveBeenCalled(); + expect(res.json).not.toHaveBeenCalled(); + expect(JSON.parse(console.log.mock.calls[0][0])).toMatchObject({ + event: 'search.performance', + requestId: 'client-disconnect-request', + outcome: 'client_disconnected', + statusCode: 499, + abortReason: 'client_disconnect', + strategy: 'standard' + }); + }); + + test('SEARCH does not start a page fetch after a client disconnects from a CPE search', async () => { + const req = new EventEmitter(); + req.requestId = 'cpe-client-disconnect-request'; + req.query = { + virtualMatchString: 'cpe:2.3:o:linux:linux_kernel', + resultsPerPage: 1, + pageNumber: 1 + }; + mockSearchRaw.mockImplementation(() => { + req.emit('aborted'); + return rawResponse(1, [ + cpeHit('CVE-2024-0001', { + criteria: 'cpe:2.3:o:linux:linux_kernel:2.5:*:*:*:*:*:*:*' + }) + ]); + }); + + await controller.SEARCH(req, res, next); + + expect(mockApiSearch).not.toHaveBeenCalled(); + expect(res.status).not.toHaveBeenCalled(); + expect(res.json).not.toHaveBeenCalled(); + }); + test('SEARCH returns sanitized errors from cve-core', async () => { const err = { name: 'SearchError', @@ -1380,11 +1519,12 @@ describe('Search controller WEB_SEARCH handler', () => { mockApiSearch.mockReset(); mockBasicSearch.mockReset(); mockSearchRaw.mockReset(); + mockSearchOpenSearch.mockClear(); mockShouldLogSearchPerformance.mockReset(); mockShouldLogSearchPerformance.mockReturnValue(false); - mockGetAdvancedSearchManager.mockClear(); mockGetBasicSearchManager.mockClear(); searchPostFilterCache.clear(); + searchInFlightRegistry.clear(); jest.spyOn(console, 'log').mockImplementation(() => {}); res = createResponse(); @@ -1392,6 +1532,7 @@ describe('Search controller WEB_SEARCH handler', () => { }); afterEach(() => { + searchInFlightRegistry.clear(); jest.restoreAllMocks(); }); diff --git a/routes/search.integration.test.js b/routes/search.integration.test.js index e2fe32e..5319044 100644 --- a/routes/search.integration.test.js +++ b/routes/search.integration.test.js @@ -1,17 +1,15 @@ const { request } = require('../test/http'); const constants = require('../utils/constants'); -const mockApiSearch = jest.fn(); +const mockSearchOpenSearch = jest.fn(); const mockBasicSearch = jest.fn(); jest.mock('../utils/searchProvider', () => ({ - getAdvancedSearchManager: jest.fn(() => ({ - apiSearch: mockApiSearch - })), getBasicSearchManager: jest.fn(() => ({ search: mockBasicSearch })), getSearchRequestDeadlineMs: jest.fn(() => 30000), + searchOpenSearch: mockSearchOpenSearch, shouldLogSearchPerformance: jest.fn(() => false) })); @@ -49,12 +47,12 @@ function basicResponse({ total = 0, hits = [] } = {}) { describe('search route integration', () => { beforeEach(() => { - mockApiSearch.mockReset(); + mockSearchOpenSearch.mockReset(); mockBasicSearch.mockReset(); }); test('POST /search validates, builds the OpenSearch request, and formats pagination metadata', async () => { - mockApiSearch.mockResolvedValue(advancedResponse(1, [{ _id: 'CVE-2024-0001' }])); + mockSearchOpenSearch.mockResolvedValue(advancedResponse(1, [{ _id: 'CVE-2024-0001' }]).data); const response = await request(createApp(), { method: 'POST', @@ -67,23 +65,31 @@ describe('search route integration', () => { }); expect(response.status).toBe(200); - expect(mockApiSearch).toHaveBeenCalledWith( - null, + expect(mockSearchOpenSearch).toHaveBeenCalledWith( { - filters: [], - rangeObjects: [], - resultsPerPage: 1, + query: { + bool: { + must: [ + { + simple_query_string: { + query: 'remote* code*', + default_operator: 'AND', + fields: ['containers.cna.descriptions.value'] + } + } + ], + filter: [] + } + }, + track_total_hits: true, + size: 1, from: 0 }, - [ - { - simple_query_string: { - query: 'remote* code*', - default_operator: 'AND', - fields: ['containers.cna.descriptions.value'] - } - } - ] + expect.objectContaining({ + operation: 'standard', + signal: expect.any(AbortSignal), + onStart: expect.any(Function) + }) ); expect(response.body.page).toBe(1); expect(response.body.pageSize).toBe(1); @@ -104,7 +110,7 @@ describe('search route integration', () => { error: 'DATE_TIME_FORMAT', message: "The pubStartDate parameter must match the following format: yyyy-MM-ddTHH:mm:ss.SSS'Z'" }); - expect(mockApiSearch).not.toHaveBeenCalled(); + expect(mockSearchOpenSearch).not.toHaveBeenCalled(); }); test('POST /search rejects excessive keyword terms before invoking OpenSearch', async () => { @@ -123,7 +129,7 @@ describe('search route integration', () => { error: 'SEARCH_FIELD_TOKEN_LIMIT', message: `The keywordSearch field must contain ${constants.MAX_KEYWORD_SEARCH_TOKENS} or fewer terms.` }); - expect(mockApiSearch).not.toHaveBeenCalled(); + expect(mockSearchOpenSearch).not.toHaveBeenCalled(); }); test('POST /search rejects an inverted virtualMatchString version range before invoking OpenSearch', async () => { @@ -145,7 +151,7 @@ describe('search route integration', () => { message: 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' }); - expect(mockApiSearch).not.toHaveBeenCalled(); + expect(mockSearchOpenSearch).not.toHaveBeenCalled(); }); test('POST /webSearch normalizes body fields and formats BasicSearchManager responses', async () => { diff --git a/utils/searchDeadline.js b/utils/searchDeadline.js index e0fafc0..ecb33c4 100644 --- a/utils/searchDeadline.js +++ b/utils/searchDeadline.js @@ -6,30 +6,95 @@ class SearchDeadlineExceededError extends Error { } } +class SearchClientDisconnectedError extends Error { + constructor() { + super('Search client disconnected.'); + this.name = 'SearchClientDisconnectedError'; + this.code = 'SEARCH_CLIENT_DISCONNECTED'; + } +} + function isSearchTimeoutError(err) { return err instanceof SearchDeadlineExceededError || err?.code === 'SEARCH_TIMEOUT' || err?.name === 'TimeoutError'; } -async function withSearchDeadline(promise, timeoutMs) { - const startedAt = Date.now(); - let timeoutId; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => reject(new SearchDeadlineExceededError()), timeoutMs); - timeoutId.unref?.(); +function isSearchClientDisconnectedError(err) { + return err instanceof SearchClientDisconnectedError || err?.code === 'SEARCH_CLIENT_DISCONNECTED'; +} + +function getAbortReason(signal) { + if (signal?.reason instanceof Error) return signal.reason; + + const err = new Error('Search request aborted.'); + err.name = 'AbortError'; + err.code = 'SEARCH_ABORTED'; + return err; +} + +function throwIfAborted(signal) { + if (signal?.aborted) throw getAbortReason(signal); +} + +function addOnceListener(emitter, eventName, listener) { + if (typeof emitter?.once !== 'function') return () => {}; + + emitter.once(eventName, listener); + return () => emitter.removeListener?.(eventName, listener); +} + +function createSearchRequestLifecycle(req, res, timeoutMs) { + const controller = new AbortController(); + let completed = false; + + const abort = (reason) => { + if (!completed && !controller.signal.aborted) { + controller.abort(reason); + } + }; + const removeRequestAbortedListener = addOnceListener(req, 'aborted', () => + abort(new SearchClientDisconnectedError()) + ); + const removeResponseCloseListener = addOnceListener(res, 'close', () => { + if (res?.writableEnded !== true) { + abort(new SearchClientDisconnectedError()); + } }); + const timeoutId = setTimeout(() => abort(new SearchDeadlineExceededError()), timeoutMs); + timeoutId.unref?.(); - try { - const result = await Promise.race([promise, timeoutPromise]); - if (Date.now() - startedAt >= timeoutMs) throw new SearchDeadlineExceededError(); + function complete() { + if (completed) return; - return result; - } finally { + completed = true; clearTimeout(timeoutId); + removeRequestAbortedListener(); + removeResponseCloseListener(); } + + function throwIfRequestAborted() { + if ( + !controller.signal.aborted && + (req?.aborted === true || (res?.destroyed === true && res?.writableEnded !== true)) + ) { + abort(new SearchClientDisconnectedError()); + } + + throwIfAborted(controller.signal); + } + + return { + complete, + signal: controller.signal, + throwIfAborted: throwIfRequestAborted + }; } module.exports = { + createSearchRequestLifecycle, + getAbortReason, + isSearchClientDisconnectedError, isSearchTimeoutError, + SearchClientDisconnectedError, SearchDeadlineExceededError, - withSearchDeadline + throwIfAborted }; diff --git a/utils/searchDeadline.test.js b/utils/searchDeadline.test.js index 944cb80..cafbf3c 100644 --- a/utils/searchDeadline.test.js +++ b/utils/searchDeadline.test.js @@ -1,3 +1,4 @@ +const { EventEmitter } = require('events'); const searchDeadline = require('./searchDeadline'); function captureTimers() { @@ -18,37 +19,90 @@ function captureTimers() { return timers; } +function createLifecycle(timeoutMs = 30000) { + const req = new EventEmitter(); + const res = new EventEmitter(); + res.writableEnded = false; + + return { + lifecycle: searchDeadline.createSearchRequestLifecycle(req, res, timeoutMs), + req, + res + }; +} + describe('searchDeadline', () => { afterEach(() => { jest.restoreAllMocks(); }); - test('returns a search result completed before the deadline', async () => { - await expect(searchDeadline.withSearchDeadline(Promise.resolve('result'), 30000)).resolves.toBe('result'); - }); - - test('rejects a search that exceeds the deadline', async () => { + test('aborts the request lifecycle when the deadline expires', () => { const timers = captureTimers(); - const pendingSearch = new Promise(() => {}); - const result = searchDeadline.withSearchDeadline(pendingSearch, 30000); - const timeoutExpectation = expect(result).rejects.toMatchObject({ + const { lifecycle } = createLifecycle(); + + timers.find(({ delay }) => delay === 30000).callback(); + + expect(lifecycle.signal.aborted).toBe(true); + expect(lifecycle.signal.reason).toMatchObject({ name: 'SearchDeadlineExceededError', code: 'SEARCH_TIMEOUT' }); + }); + + test('complete clears lifecycle listeners and prevents a late deadline abort', () => { + const timers = captureTimers(); + const { lifecycle, req, res } = createLifecycle(); + lifecycle.complete(); timers.find(({ delay }) => delay === 30000).callback(); - await timeoutExpectation; + expect(lifecycle.signal.aborted).toBe(false); + expect(req.listenerCount('aborted')).toBe(0); + expect(res.listenerCount('close')).toBe(0); + expect(clearTimeout).toHaveBeenCalledTimes(1); }); - test('rejects work that completes after the deadline before the timer can run', async () => { - jest.spyOn(Date, 'now').mockReturnValueOnce(0).mockReturnValueOnce(30001); + test('aborts when the request emits aborted', () => { + captureTimers(); + const { lifecycle, req } = createLifecycle(); - await expect(searchDeadline.withSearchDeadline(Promise.resolve('late result'), 30000)).rejects.toMatchObject({ - code: 'SEARCH_TIMEOUT' + req.emit('aborted'); + + expect(lifecycle.signal.reason).toMatchObject({ + name: 'SearchClientDisconnectedError', + code: 'SEARCH_CLIENT_DISCONNECTED' }); }); + test('aborts when the response closes before it is written', () => { + captureTimers(); + const { lifecycle, res } = createLifecycle(); + + res.emit('close'); + + expect(lifecycle.signal.reason).toMatchObject({ + code: 'SEARCH_CLIENT_DISCONNECTED' + }); + }); + + test('does not abort when a completed response closes', () => { + captureTimers(); + const { lifecycle, res } = createLifecycle(); + res.writableEnded = true; + + res.emit('close'); + + expect(lifecycle.signal.aborted).toBe(false); + }); + + test('detects an already-destroyed response before writing', () => { + captureTimers(); + const { lifecycle, res } = createLifecycle(); + res.destroyed = true; + + expect(() => lifecycle.throwIfAborted()).toThrow(searchDeadline.SearchClientDisconnectedError); + }); + test.each([ [new searchDeadline.SearchDeadlineExceededError()], [{ code: 'SEARCH_TIMEOUT' }], @@ -56,4 +110,19 @@ describe('searchDeadline', () => { ])('recognizes search timeout error %#', (err) => { expect(searchDeadline.isSearchTimeoutError(err)).toBe(true); }); + + test.each([[new searchDeadline.SearchClientDisconnectedError()], [{ code: 'SEARCH_CLIENT_DISCONNECTED' }]])( + 'recognizes client disconnect error %#', + (err) => { + expect(searchDeadline.isSearchClientDisconnectedError(err)).toBe(true); + } + ); + + test('throwIfAborted throws the lifecycle abort reason', () => { + const controller = new AbortController(); + const reason = new searchDeadline.SearchDeadlineExceededError(); + controller.abort(reason); + + expect(() => searchDeadline.throwIfAborted(controller.signal)).toThrow(reason); + }); }); diff --git a/utils/searchInFlightRegistry.js b/utils/searchInFlightRegistry.js new file mode 100644 index 0000000..07f4e85 --- /dev/null +++ b/utils/searchInFlightRegistry.js @@ -0,0 +1,118 @@ +const searchDeadline = require('./searchDeadline'); + +const entries = new Map(); + +class SearchOperationAbandonedError extends Error { + constructor() { + super('Shared search operation no longer has active waiters.'); + this.name = 'SearchOperationAbandonedError'; + this.code = 'SEARCH_OPERATION_ABANDONED'; + } +} + +function buildKey(type, value) { + return `${type}:${JSON.stringify(value)}`; +} + +function settleEntry(key, entry) { + entry.settled = true; + if (entries.get(key) === entry) { + entries.delete(key); + } +} + +function createEntry(key, createOperation) { + const controller = new AbortController(); + const entry = { + controller, + key, + promise: undefined, + settled: false, + waiterCount: 0 + }; + + entries.set(key, entry); + entry.promise = Promise.resolve().then(() => createOperation(controller.signal)); + entry.promise.then( + () => settleEntry(key, entry), + () => settleEntry(key, entry) + ); + + return entry; +} + +function releaseWaiter(entry) { + entry.waiterCount = Math.max(0, entry.waiterCount - 1); + if (!entry.settled && entry.waiterCount === 0 && !entry.controller.signal.aborted) { + settleEntry(entry.key, entry); + entry.controller.abort(new SearchOperationAbandonedError()); + } +} + +function waitForEntry(entry, signal) { + entry.waiterCount += 1; + + return new Promise((resolve, reject) => { + let settled = false; + + const finish = (callback, value) => { + if (settled) return; + + settled = true; + signal?.removeEventListener('abort', onAbort); + releaseWaiter(entry); + callback(value); + }; + const onAbort = () => finish(reject, searchDeadline.getAbortReason(signal)); + + if (signal?.aborted) { + onAbort(); + return; + } + + signal?.addEventListener('abort', onAbort, { once: true }); + entry.promise.then( + (value) => finish(resolve, value), + (err) => finish(reject, err) + ); + }); +} + +function acquire(key, createOperation, { signal } = {}) { + searchDeadline.throwIfAborted(signal); + + let entry = entries.get(key); + const shared = Boolean(entry); + if (!entry) { + entry = createEntry(key, createOperation); + } + + return { + promise: waitForEntry(entry, signal), + shared + }; +} + +function clear() { + for (const entry of entries.values()) { + if (!entry.controller.signal.aborted) { + entry.controller.abort(new SearchOperationAbandonedError()); + } + } + entries.clear(); +} + +function getState() { + return { + operationCount: entries.size, + waiterCount: [...entries.values()].reduce((total, entry) => total + entry.waiterCount, 0) + }; +} + +module.exports = { + acquire, + buildKey, + clear, + getState, + SearchOperationAbandonedError +}; diff --git a/utils/searchInFlightRegistry.test.js b/utils/searchInFlightRegistry.test.js new file mode 100644 index 0000000..aed7ad9 --- /dev/null +++ b/utils/searchInFlightRegistry.test.js @@ -0,0 +1,133 @@ +const searchDeadline = require('./searchDeadline'); +const searchInFlightRegistry = require('./searchInFlightRegistry'); + +function deferred() { + const state = {}; + state.promise = new Promise((resolve, reject) => { + state.resolve = resolve; + state.reject = reject; + }); + return state; +} + +describe('searchInFlightRegistry', () => { + beforeEach(() => { + searchInFlightRegistry.clear(); + }); + + afterEach(() => { + searchInFlightRegistry.clear(); + }); + + test('coalesces waiters for the same operation key', async () => { + const operation = deferred(); + const createOperation = jest.fn(() => operation.promise); + const first = searchInFlightRegistry.acquire('same-key', createOperation); + const second = searchInFlightRegistry.acquire('same-key', createOperation); + await Promise.resolve(); + + expect(first.shared).toBe(false); + expect(second.shared).toBe(true); + expect(createOperation).toHaveBeenCalledTimes(1); + expect(searchInFlightRegistry.getState()).toStrictEqual({ + operationCount: 1, + waiterCount: 2 + }); + + operation.resolve('result'); + + await expect(first.promise).resolves.toBe('result'); + await expect(second.promise).resolves.toBe('result'); + expect(searchInFlightRegistry.getState()).toStrictEqual({ + operationCount: 0, + waiterCount: 0 + }); + }); + + test('keeps shared work active while another waiter remains', async () => { + const operation = deferred(); + const firstController = new AbortController(); + const secondController = new AbortController(); + let operationSignal; + const createOperation = jest.fn((signal) => { + operationSignal = signal; + return operation.promise; + }); + const first = searchInFlightRegistry.acquire('same-key', createOperation, { + signal: firstController.signal + }); + const second = searchInFlightRegistry.acquire('same-key', createOperation, { + signal: secondController.signal + }); + await Promise.resolve(); + + firstController.abort(new searchDeadline.SearchClientDisconnectedError()); + + await expect(first.promise).rejects.toMatchObject({ + code: 'SEARCH_CLIENT_DISCONNECTED' + }); + expect(operationSignal.aborted).toBe(false); + expect(searchInFlightRegistry.getState().waiterCount).toBe(1); + + operation.resolve('result'); + await expect(second.promise).resolves.toBe('result'); + }); + + test('aborts shared work after its final waiter cancels', async () => { + const operation = deferred(); + const controller = new AbortController(); + let operationSignal; + const acquired = searchInFlightRegistry.acquire( + 'same-key', + (signal) => { + operationSignal = signal; + return operation.promise; + }, + { + signal: controller.signal + } + ); + await Promise.resolve(); + + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + await expect(acquired.promise).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + expect(operationSignal.reason).toMatchObject({ + code: 'SEARCH_OPERATION_ABANDONED' + }); + expect(searchInFlightRegistry.getState().operationCount).toBe(0); + + const replacementOperation = jest.fn(() => Promise.resolve('replacement')); + const replacement = searchInFlightRegistry.acquire('same-key', replacementOperation); + + expect(replacement.shared).toBe(false); + await expect(replacement.promise).resolves.toBe('replacement'); + expect(replacementOperation).toHaveBeenCalledTimes(1); + + operation.reject(operationSignal.reason); + await expect(operation.promise).rejects.toMatchObject({ + code: 'SEARCH_OPERATION_ABANDONED' + }); + }); + + test('does not start work for an already-aborted waiter', () => { + const controller = new AbortController(); + const createOperation = jest.fn(); + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + expect(() => + searchInFlightRegistry.acquire('same-key', createOperation, { + signal: controller.signal + }) + ).toThrow(searchDeadline.SearchDeadlineExceededError); + expect(createOperation).not.toHaveBeenCalled(); + }); + + test('buildKey preserves operation type boundaries', () => { + expect(searchInFlightRegistry.buildKey('candidate', ['CVE-2024-0001'])).not.toBe( + searchInFlightRegistry.buildKey('pageFetch', ['CVE-2024-0001']) + ); + }); +}); diff --git a/utils/searchPerformance.js b/utils/searchPerformance.js index 26ebd22..1240cfc 100644 --- a/utils/searchPerformance.js +++ b/utils/searchPerformance.js @@ -32,6 +32,7 @@ function logSearchPerformance(metrics, searchOptions, responseOptions, postFilte outcome, statusCode: metrics.statusCode, timeoutSource: metrics.timeoutSource, + abortReason: metrics.abortReason, strategy: metrics.strategy, cacheStatus: getCacheStatus(metrics, postFilters), coldSearchQueueWaitElapsedMs: roundMs(metrics.coldSearchQueueWaitElapsedMs), @@ -45,6 +46,8 @@ function logSearchPerformance(metrics, searchOptions, responseOptions, postFilte cacheHit: metrics.cacheHit === true, inFlightHit: metrics.inFlightHit === true, pageFetchInFlightHit: metrics.pageFetchInFlightHit === true, + openSearchOperations: metrics.openSearchOperations ?? [], + maxOpenSearchActiveRequests: metrics.maxOpenSearchActiveRequests, postFilters: Object.keys(postFilters ?? {}), requestedFrom: responseOptions?.from ?? searchOptions?.from, requestedSize: responseOptions?.resultsPerPage ?? searchOptions?.resultsPerPage, diff --git a/utils/searchPerformance.test.js b/utils/searchPerformance.test.js index b4f1cfa..5ec42f1 100644 --- a/utils/searchPerformance.test.js +++ b/utils/searchPerformance.test.js @@ -28,6 +28,8 @@ describe('searchPerformance', () => { cacheHit: true, inFlightHit: true, pageFetchInFlightHit: true, + openSearchOperations: ['cpe-candidate', 'page-fetch'], + maxOpenSearchActiveRequests: 3, candidateCount: 10000, filteredCount: 4, returnedCount: 1, @@ -72,6 +74,8 @@ describe('searchPerformance', () => { cacheHit: true, inFlightHit: true, pageFetchInFlightHit: true, + openSearchOperations: ['cpe-candidate', 'page-fetch'], + maxOpenSearchActiveRequests: 3, postFilters: ['virtualMatchStringRange'], requestedFrom: 0, requestedSize: 1, diff --git a/utils/searchPostFilterCache.js b/utils/searchPostFilterCache.js index a573859..f7ed9d8 100644 --- a/utils/searchPostFilterCache.js +++ b/utils/searchPostFilterCache.js @@ -1,7 +1,6 @@ const searchProvider = require('./searchProvider'); const entries = new Map(); -const inFlightEntries = new Map(); function normalizeForKey(value) { if (Array.isArray(value)) return value.map(normalizeForKey); @@ -29,15 +28,6 @@ function buildCacheKey(candidateBody, postFilters) { ); } -function buildInFlightKey(type, value) { - return JSON.stringify( - normalizeForKey({ - type, - value - }) - ); -} - function get(cacheKey, now = Date.now()) { const entry = entries.get(cacheKey); if (!entry) return null; @@ -70,36 +60,14 @@ function set(cacheKey, value, now = Date.now()) { } } -function getInFlight(cacheKey) { - return inFlightEntries.get(cacheKey); -} - -function setInFlight(cacheKey, promise) { - inFlightEntries.set(cacheKey, promise); - - const clear = () => { - if (inFlightEntries.get(cacheKey) === promise) { - inFlightEntries.delete(cacheKey); - } - }; - - promise.then(clear, clear); - - return promise; -} - function clear() { entries.clear(); - inFlightEntries.clear(); } module.exports = { buildCacheKey, - buildInFlightKey, clear, get, - getInFlight, normalizeForKey, - set, - setInFlight + set }; diff --git a/utils/searchPostFilterCache.test.js b/utils/searchPostFilterCache.test.js index 50a4737..77aff79 100644 --- a/utils/searchPostFilterCache.test.js +++ b/utils/searchPostFilterCache.test.js @@ -1,14 +1,5 @@ const searchPostFilterCache = require('./searchPostFilterCache'); -function deferred() { - const state = {}; - state.promise = new Promise((resolve, reject) => { - state.resolve = resolve; - state.reject = reject; - }); - return state; -} - describe('searchPostFilterCache', () => { beforeEach(() => { searchPostFilterCache.clear(); @@ -55,12 +46,6 @@ describe('searchPostFilterCache', () => { expect(left).toBe(right); }); - test('buildInFlightKey is stable for equivalent structured values', () => { - expect(searchPostFilterCache.buildInFlightKey('pageFetch', ['CVE-2024-0001'])).toBe( - searchPostFilterCache.buildInFlightKey('pageFetch', ['CVE-2024-0001']) - ); - }); - test('get returns cached values until the entry expires', () => { const key = 'cache-key'; const value = { @@ -73,33 +58,4 @@ describe('searchPostFilterCache', () => { expect(searchPostFilterCache.get(key, 1001)).toBe(value); expect(searchPostFilterCache.get(key, 61001)).toBeNull(); }); - - test('setInFlight returns an existing in-flight promise until it resolves', async () => { - const pending = deferred(); - - searchPostFilterCache.setInFlight('cache-key', pending.promise); - - expect(searchPostFilterCache.getInFlight('cache-key')).toBe(pending.promise); - - pending.resolve({ - candidateCount: 1, - filteredCveIds: ['CVE-2024-0001'] - }); - await pending.promise; - await Promise.resolve(); - - expect(searchPostFilterCache.getInFlight('cache-key')).toBeUndefined(); - }); - - test('setInFlight clears rejected in-flight promises', async () => { - const pending = deferred(); - - searchPostFilterCache.setInFlight('cache-key', pending.promise); - - pending.reject(new Error('candidate search failed')); - await expect(pending.promise).rejects.toThrow('candidate search failed'); - await Promise.resolve(); - - expect(searchPostFilterCache.getInFlight('cache-key')).toBeUndefined(); - }); }); diff --git a/utils/searchPostFilterWorkQueue.js b/utils/searchPostFilterWorkQueue.js index b89ae86..8180f97 100644 --- a/utils/searchPostFilterWorkQueue.js +++ b/utils/searchPostFilterWorkQueue.js @@ -1,4 +1,5 @@ const searchProvider = require('./searchProvider'); +const searchDeadline = require('./searchDeadline'); let activeCount = 0; const queue = []; @@ -58,11 +59,13 @@ function drain() { while (activeCount < maxConcurrent && queue.length) { const item = queue.shift(); clearTimeout(item.timeoutId); + item.signal?.removeEventListener('abort', item.onAbort); activeCount += 1; void (async () => { try { - item.resolve(await item.task()); + searchDeadline.throwIfAborted(item.signal); + item.resolve(await item.task(item.signal)); } catch (err) { item.reject(err); } finally { @@ -73,8 +76,15 @@ function drain() { } } -function run(task) { +function run(task, { signal } = {}) { return new Promise((resolve, reject) => { + try { + searchDeadline.throwIfAborted(signal); + } catch (err) { + reject(err); + return; + } + if (activeCount >= getMaxConcurrent() && queue.length >= getMaxQueueDepth()) { reject(new SearchPostFilterQueueFullError(getState())); return; @@ -85,15 +95,28 @@ function run(task) { resolve, reject, queuedAt: process.hrtime.bigint(), - timeoutId: undefined + timeoutId: undefined, + signal, + onAbort: undefined }; if (activeCount >= getMaxConcurrent()) { + item.onAbort = () => { + const queueIndex = queue.indexOf(item); + if (queueIndex === -1) return; + + queue.splice(queueIndex, 1); + clearTimeout(item.timeoutId); + reject(searchDeadline.getAbortReason(signal)); + drain(); + }; + signal?.addEventListener('abort', item.onAbort, { once: true }); item.timeoutId = setTimeout(() => { const queueIndex = queue.indexOf(item); if (queueIndex === -1) return; queue.splice(queueIndex, 1); + signal?.removeEventListener('abort', item.onAbort); const waitElapsedMs = Number(process.hrtime.bigint() - item.queuedAt) / 1e6; reject(new SearchPostFilterQueueTimeoutError(getState(), waitElapsedMs)); drain(); @@ -108,7 +131,10 @@ function run(task) { function resetForTests() { activeCount = 0; - queue.forEach((item) => clearTimeout(item.timeoutId)); + queue.forEach((item) => { + clearTimeout(item.timeoutId); + item.signal?.removeEventListener('abort', item.onAbort); + }); queue.length = 0; } diff --git a/utils/searchPostFilterWorkQueue.test.js b/utils/searchPostFilterWorkQueue.test.js index b8406f3..02ef0b1 100644 --- a/utils/searchPostFilterWorkQueue.test.js +++ b/utils/searchPostFilterWorkQueue.test.js @@ -9,6 +9,7 @@ jest.mock('./searchProvider', () => ({ })); const searchPostFilterWorkQueue = require('./searchPostFilterWorkQueue'); +const searchDeadline = require('./searchDeadline'); function deferred() { const state = {}; @@ -152,4 +153,48 @@ describe('searchPostFilterWorkQueue', () => { first.resolve('first'); await expect(firstRun).resolves.toBe('first'); }); + + test('removes queued work when its operation is aborted', async () => { + const first = deferred(); + const controller = new AbortController(); + const secondTask = jest.fn(); + + const firstRun = searchPostFilterWorkQueue.run(() => first.promise); + const secondRun = searchPostFilterWorkQueue.run(secondTask, { + signal: controller.signal + }); + await Promise.resolve(); + + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + await expect(secondRun).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + expect(secondTask).not.toHaveBeenCalled(); + expect(searchPostFilterWorkQueue.getState().queuedCount).toBe(0); + + first.resolve('first'); + await expect(firstRun).resolves.toBe('first'); + }); + + test('passes the operation signal to active work', async () => { + const controller = new AbortController(); + const task = jest.fn( + (signal) => + new Promise((resolve, reject) => { + signal.addEventListener('abort', () => reject(signal.reason), { once: true }); + }) + ); + const run = searchPostFilterWorkQueue.run(task, { + signal: controller.signal + }); + await Promise.resolve(); + + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + await expect(run).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + expect(task).toHaveBeenCalledWith(controller.signal); + }); }); diff --git a/utils/searchProvider.js b/utils/searchProvider.js index e056387..9b23133 100644 --- a/utils/searchProvider.js +++ b/utils/searchProvider.js @@ -1,10 +1,12 @@ -const { AdvancedSearchManager, AppConfig, BasicSearchManager, SearchProviderSpec, SearchReader } = require('cve-core'); +const { AppConfig, BasicSearchManager, SearchProviderSpec, SearchReader } = require('cve-core'); const constants = require('./constants'); +const searchDeadline = require('./searchDeadline'); let searchProviderConfig; -let advancedSearchManager; let basicSearchManager; let searchReader; +let activeOpenSearchRequestCount = 0; +const activeOpenSearchRequestsByOperation = new Map(); function configureClientRequestTimeout(client) { if (client?.transport) { @@ -75,14 +77,6 @@ function getSearchProviderConfig() { return searchProviderConfig; } -function getAdvancedSearchManager() { - if (!advancedSearchManager) { - advancedSearchManager = configureManagerRequestTimeout(new AdvancedSearchManager(getSearchProviderConfig())); - } - - return advancedSearchManager; -} - function getBasicSearchManager() { if (!basicSearchManager) { basicSearchManager = configureManagerRequestTimeout(new BasicSearchManager(getSearchProviderConfig())); @@ -103,20 +97,86 @@ function getSearchReader() { return searchReader; } -async function searchRaw(body) { - const reader = getSearchReader(); - const response = await reader._client.search( - { - index: reader._cveIndex, - body - }, - { - maxRetries: 0, - requestTimeout: getOpenSearchRequestTimeoutMs() - } - ); +function getOpenSearchRequestState() { + return { + activeCount: activeOpenSearchRequestCount, + activeByOperation: Object.fromEntries(activeOpenSearchRequestsByOperation) + }; +} + +function beginOpenSearchRequest(operation) { + activeOpenSearchRequestCount += 1; + activeOpenSearchRequestsByOperation.set(operation, (activeOpenSearchRequestsByOperation.get(operation) ?? 0) + 1); + + return getOpenSearchRequestState(); +} - return response.body; +function endOpenSearchRequest(operation) { + activeOpenSearchRequestCount = Math.max(0, activeOpenSearchRequestCount - 1); + + const operationCount = activeOpenSearchRequestsByOperation.get(operation) ?? 0; + if (operationCount <= 1) { + activeOpenSearchRequestsByOperation.delete(operation); + } else { + activeOpenSearchRequestsByOperation.set(operation, operationCount - 1); + } +} + +function awaitAbortableRequest(request, signal) { + if (!signal) return Promise.resolve(request); + + searchDeadline.throwIfAborted(signal); + + return new Promise((resolve, reject) => { + let settled = false; + + const finish = (callback, value) => { + if (settled) return; + + settled = true; + signal.removeEventListener('abort', onAbort); + callback(value); + }; + const onAbort = () => { + try { + request?.abort?.(); + } catch (_err) { + // The request still rejects with the lifecycle's sanitized abort reason. + } + finish(reject, searchDeadline.getAbortReason(signal)); + }; + + signal.addEventListener('abort', onAbort, { once: true }); + Promise.resolve(request).then( + (response) => finish(resolve, response), + (err) => finish(reject, signal.aborted ? searchDeadline.getAbortReason(signal) : err) + ); + }); +} + +async function searchOpenSearch(body, { signal, operation = 'search', onStart } = {}) { + searchDeadline.throwIfAborted(signal); + const reader = getSearchReader(); + const state = beginOpenSearchRequest(operation); + + try { + onStart?.(state); + const request = reader._client.search( + { + index: reader._cveIndex, + body + }, + { + maxRetries: 0, + requestTimeout: getOpenSearchRequestTimeoutMs() + } + ); + const response = await awaitAbortableRequest(request, signal); + + return response.body; + } finally { + endOpenSearchRequest(operation); + } } function shouldLogSearchPerformance() { @@ -169,16 +229,17 @@ function getSearchRequestDeadlineMs() { function resetSearchProviderForTests() { searchProviderConfig = undefined; - advancedSearchManager = undefined; basicSearchManager = undefined; searchReader = undefined; + activeOpenSearchRequestCount = 0; + activeOpenSearchRequestsByOperation.clear(); } module.exports = { - getAdvancedSearchManager, getBasicSearchManager, getAllowUnknownSslCerts, getIntegerConfig, + getOpenSearchRequestState, getPostFilterCacheMaxEntries, getPostFilterCacheTtlMs, getPostFilterColdSearchConcurrency, @@ -190,6 +251,6 @@ module.exports = { getSearchReader, normalizeBoolean, resetSearchProviderForTests, - searchRaw, + searchOpenSearch, shouldLogSearchPerformance }; diff --git a/utils/searchProvider.test.js b/utils/searchProvider.test.js index 56899d7..b6f7bbf 100644 --- a/utils/searchProvider.test.js +++ b/utils/searchProvider.test.js @@ -1,14 +1,3 @@ -const mockAdvancedSearchManager = jest.fn(function AdvancedSearchManager(spec) { - this.spec = spec; - this._searchReader = { - _client: { - transport: { - maxRetries: 3, - requestTimeout: 30000 - } - } - }; -}); const mockBasicSearchManager = jest.fn(function BasicSearchManager(spec) { this.spec = spec; this._searchReader = { @@ -41,7 +30,6 @@ const mockSearchProviderSpec = { }; jest.mock('cve-core', () => ({ - AdvancedSearchManager: mockAdvancedSearchManager, AppConfig: { get: mockAppConfigGet, has: mockAppConfigHas @@ -52,11 +40,20 @@ jest.mock('cve-core', () => ({ })); const searchProvider = require('./searchProvider'); +const searchDeadline = require('./searchDeadline'); + +function deferred() { + const state = {}; + state.promise = new Promise((resolve, reject) => { + state.resolve = resolve; + state.reject = reject; + }); + return state; +} describe('searchProvider', () => { beforeEach(() => { searchProvider.resetSearchProviderForTests(); - mockAdvancedSearchManager.mockClear(); mockBasicSearchManager.mockClear(); mockSearchReader.mockClear(); mockClientSearch.mockReset(); @@ -91,22 +88,6 @@ describe('searchProvider', () => { expect(mockGetDefaultSearchProviderSpec).toHaveBeenCalledTimes(1); }); - test('reuses one advanced search manager per process', () => { - const first = searchProvider.getAdvancedSearchManager(); - const second = searchProvider.getAdvancedSearchManager(); - - expect(first).toBe(second); - expect(mockAdvancedSearchManager).toHaveBeenCalledTimes(1); - expect(mockAdvancedSearchManager).toHaveBeenCalledWith({ - providerEndpoint: 'https://search.example', - index: 'test-index', - allowUnknownSslCerts: false - }); - expect(mockGetDefaultSearchProviderSpec).toHaveBeenCalledTimes(1); - expect(first._searchReader._client.transport.maxRetries).toBe(0); - expect(first._searchReader._client.transport.requestTimeout).toBe(20000); - }); - test('reuses one basic search manager per process', () => { const first = searchProvider.getBasicSearchManager(); const second = searchProvider.getBasicSearchManager(); @@ -123,7 +104,7 @@ describe('searchProvider', () => { expect(first._searchReader._client.transport.requestTimeout).toBe(20000); }); - test('reuses one search reader per process for raw OpenSearch searches', async () => { + test('reuses one search reader per process for OpenSearch searches', async () => { mockClientSearch.mockResolvedValue({ body: { hits: { @@ -142,7 +123,11 @@ describe('searchProvider', () => { match_all: {} } }; - const result = await searchProvider.searchRaw(body); + const onStart = jest.fn(); + const result = await searchProvider.searchOpenSearch(body, { + operation: 'standard', + onStart + }); expect(first).toBe(second); expect(mockSearchReader).toHaveBeenCalledTimes(1); @@ -169,6 +154,115 @@ describe('searchProvider', () => { hits: [] } }); + expect(onStart).toHaveBeenCalledWith({ + activeCount: 1, + activeByOperation: { + standard: 1 + } + }); + expect(searchProvider.getOpenSearchRequestState()).toStrictEqual({ + activeCount: 0, + activeByOperation: {} + }); + }); + + test('aborts an active OpenSearch request when the lifecycle signal aborts', async () => { + const pending = deferred(); + const request = pending.promise; + request.abort = jest.fn(); + mockClientSearch.mockReturnValue(request); + const controller = new AbortController(); + const search = searchProvider.searchOpenSearch( + { + query: { + match_all: {} + } + }, + { + signal: controller.signal, + operation: 'standard' + } + ); + await Promise.resolve(); + + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + await expect(search).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + expect(request.abort).toHaveBeenCalledTimes(1); + expect(searchProvider.getOpenSearchRequestState()).toStrictEqual({ + activeCount: 0, + activeByOperation: {} + }); + }); + + test('tracks concurrent OpenSearch requests by operation type', async () => { + const candidate = deferred(); + const pageFetch = deferred(); + const candidateStart = jest.fn(); + const pageFetchStart = jest.fn(); + mockClientSearch.mockReturnValueOnce(candidate.promise).mockReturnValueOnce(pageFetch.promise); + + const candidateSearch = searchProvider.searchOpenSearch( + {}, + { + operation: 'cpe-candidate', + onStart: candidateStart + } + ); + const pageSearch = searchProvider.searchOpenSearch( + {}, + { + operation: 'page-fetch', + onStart: pageFetchStart + } + ); + + expect(candidateStart).toHaveBeenCalledWith({ + activeCount: 1, + activeByOperation: { + 'cpe-candidate': 1 + } + }); + expect(pageFetchStart).toHaveBeenCalledWith({ + activeCount: 2, + activeByOperation: { + 'cpe-candidate': 1, + 'page-fetch': 1 + } + }); + + candidate.resolve({ body: { hits: { hits: [] } } }); + pageFetch.resolve({ body: { hits: { hits: [] } } }); + await Promise.all([candidateSearch, pageSearch]); + + expect(searchProvider.getOpenSearchRequestState()).toStrictEqual({ + activeCount: 0, + activeByOperation: {} + }); + }); + + test('does not start an OpenSearch request for an already-aborted signal', async () => { + const controller = new AbortController(); + controller.abort(new searchDeadline.SearchClientDisconnectedError()); + + await expect( + searchProvider.searchOpenSearch( + { + query: { + match_all: {} + } + }, + { + signal: controller.signal + } + ) + ).rejects.toMatchObject({ + code: 'SEARCH_CLIENT_DISCONNECTED' + }); + expect(mockSearchReader).not.toHaveBeenCalled(); + expect(mockClientSearch).not.toHaveBeenCalled(); }); test('throws when OpenSearch endpoint or index are missing', () => { diff --git a/utils/searchQueryBuilder.test.js b/utils/searchQueryBuilder.test.js index 2c214bc..d8f9e31 100644 --- a/utils/searchQueryBuilder.test.js +++ b/utils/searchQueryBuilder.test.js @@ -229,6 +229,41 @@ describe('searchQueryBuilder buildSearchRequest()', () => { ); }); + test('buildOpenSearchBody builds the complete standard search request', () => { + const output = queryBuilder.buildSearchRequest({ + keywordSearch: 'remote code', + cweId: 'CWE-79', + resultsPerPage: 10, + pageNumber: 3 + }); + + expect(queryBuilder.buildOpenSearchBody(output.searchOptions, output.queryStringObjects)).toStrictEqual({ + query: { + bool: { + must: [ + { + simple_query_string: { + query: 'remote* code*', + default_operator: 'AND', + fields: ['containers.cna.descriptions.value'] + } + } + ], + filter: [ + { + match_phrase: { + 'containers.cna.problemTypes.descriptions.cweId': 'CWE-79' + } + } + ] + } + }, + track_total_hits: true, + size: 10, + from: 20 + }); + }); + test('buildCveIdsSearchRequest builds exact page fetch request for selected CVE IDs', () => { expect(queryBuilder.buildCveIdsSearchRequest(['CVE-2024-0002', 'CVE-2024-0001'])).toStrictEqual({ searchOptions: { From 01a0c0aae51800040c8586d96b70b5a4d09ddd43 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 09:54:59 -0400 Subject: [PATCH 12/20] feat: add OpenSearch readiness checks --- .env.example | 1 + README.md | 11 +- api-docs/openapi.json | 41 +++++++- api-docs/openapi.test.js | 3 + config/custom-environment-variables.jsonc | 3 +- config/default.jsonc | 4 +- config/runtime-config.test.js | 1 + controllers/searchController.js | 1 + controllers/searchController.test.js | 4 + routes/index.js | 71 ++++++++++++- routes/index.test.js | 36 +++++++ utils/constants.js | 2 + utils/errors.js | 4 + utils/errors.test.js | 8 ++ utils/requestLogging.js | 3 +- utils/requestLogging.test.js | 58 ++++++----- utils/searchProvider.js | 120 ++++++++++++++++++---- utils/searchProvider.test.js | 100 +++++++++++++++++- 18 files changed, 415 insertions(+), 56 deletions(-) diff --git a/.env.example b/.env.example index 0b81c9b..a90f4b1 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ SearchPostFilterColdSearchMaxQueueDepth=500 SearchPostFilterColdSearchMaxWaitMs=10000 SearchOpenSearchRequestTimeoutMs=20000 SearchRequestDeadlineMs=30000 +SearchReadinessTimeoutMs=5000 RequestLogSuccessSampleRate=0.1 RequestLogHealthChecks=false WebSearchEnabled=true diff --git a/README.md b/README.md index 9903dd3..21080c4 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Rename `.env.example` to `.env` and replace or check the following values. - `SearchPostFilterColdSearchMaxWaitMs` - optional maximum cold CPE candidate queue wait, default `10000` - `SearchOpenSearchRequestTimeoutMs` - optional timeout for each OpenSearch request, default `20000` - `SearchRequestDeadlineMs` - optional overall `/search` processing deadline, default `30000` +- `SearchReadinessTimeoutMs` - optional timeout for the OpenSearch index readiness check, default `5000` - `RequestLogSuccessSampleRate` - optional fraction of successful non-health-check access logs to retain, default `0.1` - `RequestLogHealthChecks` - optional flag to retain successful health-check access logs, default `false` - `WebSearchEnabled` - optional deprecated `/webSearch` feature flag, default `true` @@ -84,6 +85,14 @@ npm start The Search API will be served at `localhost:3000`. +## Health Checks + +`GET /api/health-check` is a process liveness check. It returns HTTP 200 when the Node.js process is running and deliberately does not contact OpenSearch. Use it where a dependency outage should not restart otherwise healthy API tasks. + +`GET /api/ready-check` sends a minimal zero-result search to the configured CVE index. This verifies connectivity, index availability, and the same search permission used by the API without retrieving CVE records or counting all matches. It returns HTTP 200 when search requests can be served. Configuration errors, connection failures, timeouts, incompatible `cve-core` SearchReader contracts, and a missing index return a sanitized HTTP 503 `SEARCH_NOT_READY` response with `Retry-After: 5`. The OpenSearch request is limited to five seconds by default and can be changed with `SearchReadinessTimeoutMs`. + +An AWS load balancer can use `/api/ready-check` to stop routing search traffic to an unready task while the container orchestrator continues using `/api/health-check` for process liveness. Coordinate that probe selection with the infrastructure team before changing deployed health-check behavior. + ## API Input Validation `POST /search` accepts an optional JSON object request body and validates supported fields, boolean parameters, pagination values, CVSS severity values, required parameter combinations, CPE Name requirements, and date-time formats. A non-empty body must use the `application/json` media type with UTF-8 encoding; unsupported media types or encodings return HTTP 415, and malformed JSON returns HTTP 400. If OpenSearch is unavailable, the endpoint returns HTTP 503 with a `Retry-After` header. @@ -231,7 +240,7 @@ npm run quality Every request receives an `X-Request-ID` response header. A caller-provided `X-Request-ID` is preserved when it contains only letters, numbers, `.`, `_`, `:`, or `-` and is no longer than 128 characters; otherwise the API generates a UUID. The same value appears as `request-id` in access logs and `requestId` in structured `/search` events. -Access logs omit query strings and request bodies. By default, the API retains 10% of successful non-health-check access logs, suppresses successful `/api/health-check` access logs, and retains every HTTP 4xx or 5xx access log. Set `RequestLogSuccessSampleRate` from `0` through `1` to change successful-request sampling. Set `RequestLogHealthChecks=true` to retain successful health-check logs. +Access logs omit query strings and request bodies. By default, the API retains 10% of successful non-health-check access logs, suppresses successful `/api/health-check` and `/api/ready-check` access logs, and retains every HTTP 4xx or 5xx access log. Set `RequestLogSuccessSampleRate` from `0` through `1` to change successful-request sampling. Set `RequestLogHealthChecks=true` to retain successful health-check logs. The API always emits a sanitized JSON `search.performance` event for `/search` queue saturation, queue wait timeout, search timeout, client disconnect, OpenSearch connection errors, and upstream search errors. These events include the request ID, outcome, HTTP status, strategy, cache state, total elapsed time, cancellation reason, and queue state when applicable. Search terms, CPE values, request bodies, credentials, and upstream error details are never included. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 660fd07..a860ab1 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -157,8 +157,8 @@ }, "/api/health-check": { "get": { - "summary": "Health check for AWS monitoring", - "description": "Health check for AWS monitoring", + "summary": "Process liveness check", + "description": "Returns 200 when the API process is running. This endpoint does not check OpenSearch.", "operationId": "healthCheck", "responses": { "200": { @@ -175,6 +175,43 @@ } } }, + "/api/ready-check": { + "get": { + "summary": "OpenSearch readiness check", + "description": "Sends a minimal zero-result search to the configured CVE index. Returns 200 when the index is available with search permission, or a sanitized 503 response when configuration, connectivity, timeout, permission, or index availability checks fail.", + "operationId": "readinessCheck", + "responses": { + "200": { + "description": "API is ready to serve search requests", + "headers": { + "X-Request-ID": { + "description": "Request correlation ID", + "schema": { + "type": "string" + } + } + } + }, + "503": { + "description": "Search service is not ready", + "headers": { + "Retry-After": { + "description": "Seconds before retrying the readiness check", + "schema": { + "type": "integer" + } + }, + "X-Request-ID": { + "description": "Request correlation ID", + "schema": { + "type": "string" + } + } + } + } + } + } + }, "/webSearch": { "post": { "tags": [ diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index a6518ac..3b205f1 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -20,6 +20,7 @@ describe('OpenAPI contract', () => { test('documents the supported route surface', () => { expect(openapi.paths['/search'].post.operationId).toBe('search'); expect(openapi.paths['/api/health-check'].get.operationId).toBe('healthCheck'); + expect(openapi.paths['/api/ready-check'].get.operationId).toBe('readinessCheck'); expect(openapi.paths['/webSearch'].post.operationId).toBe('webSearch'); }); @@ -143,6 +144,8 @@ describe('OpenAPI contract', () => { } }); expect(openapi.paths['/api/health-check'].get.responses['200'].headers['X-Request-ID']).toBeDefined(); + expect(openapi.paths['/api/ready-check'].get.responses['200'].headers['X-Request-ID']).toBeDefined(); + expect(openapi.paths['/api/ready-check'].get.responses['503'].headers['Retry-After']).toBeDefined(); expect(openapi.paths['/webSearch'].post.responses['200'].headers['X-Request-ID']).toBeDefined(); }); diff --git a/config/custom-environment-variables.jsonc b/config/custom-environment-variables.jsonc index a194540..3470a97 100644 --- a/config/custom-environment-variables.jsonc +++ b/config/custom-environment-variables.jsonc @@ -18,7 +18,8 @@ "postFilterColdSearchMaxQueueDepth": "SearchPostFilterColdSearchMaxQueueDepth", "postFilterColdSearchMaxWaitMs": "SearchPostFilterColdSearchMaxWaitMs", "openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs", - "requestDeadlineMs": "SearchRequestDeadlineMs" + "requestDeadlineMs": "SearchRequestDeadlineMs", + "readinessTimeoutMs": "SearchReadinessTimeoutMs" }, "requestLogging": { "successSampleRate": "RequestLogSuccessSampleRate", diff --git a/config/default.jsonc b/config/default.jsonc index 124a926..8cdef78 100644 --- a/config/default.jsonc +++ b/config/default.jsonc @@ -28,7 +28,9 @@ // Limits each request from the API process to OpenSearch. "openSearchRequestTimeoutMs": "20000", // Limits total processing time for a /search request. - "requestDeadlineMs": "30000" + "requestDeadlineMs": "30000", + // Limits the minimal OpenSearch request used by /api/ready-check. + "readinessTimeoutMs": "5000" }, // Access logging settings. Error responses are always logged. "requestLogging": { diff --git a/config/runtime-config.test.js b/config/runtime-config.test.js index 67946b3..350a6b1 100644 --- a/config/runtime-config.test.js +++ b/config/runtime-config.test.js @@ -102,6 +102,7 @@ describe('runtime container and compose configuration', () => { '"openSearchRequestTimeoutMs": "SearchOpenSearchRequestTimeoutMs"' ); expect(customEnvironmentVariables).toContain('"requestDeadlineMs": "SearchRequestDeadlineMs"'); + expect(customEnvironmentVariables).toContain('"readinessTimeoutMs": "SearchReadinessTimeoutMs"'); expect(customEnvironmentVariables).toContain('"successSampleRate": "RequestLogSuccessSampleRate"'); expect(customEnvironmentVariables).toContain('"healthChecks": "RequestLogHealthChecks"'); expect(customEnvironmentVariables).toContain('"webSearchEnabled": "WebSearchEnabled"'); diff --git a/controllers/searchController.js b/controllers/searchController.js index a76564b..5deabdb 100644 --- a/controllers/searchController.js +++ b/controllers/searchController.js @@ -105,6 +105,7 @@ function getOpenSearchErrorStatusCode(err) { function isOpenSearchUnavailableError(err) { return ( + searchProvider.isSearchProviderContractError?.(err) === true || openSearchUnavailableErrorNames.has(err?.name) || openSearchUnavailableStatusCodes.has(getOpenSearchErrorStatusCode(err)) ); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 387703b..1e5fe87 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -22,6 +22,7 @@ const mockGetPostFilterColdSearchConcurrency = jest.fn(() => 10); const mockGetPostFilterColdSearchMaxQueueDepth = jest.fn(() => 500); const mockGetPostFilterColdSearchMaxWaitMs = jest.fn(() => 10000); const mockGetSearchRequestDeadlineMs = jest.fn(() => 30000); +const mockIsSearchProviderContractError = jest.fn((err) => err?.code === 'SEARCH_PROVIDER_CONTRACT'); const mockGetBasicSearchManager = jest.fn(() => ({ search: mockBasicSearch })); @@ -42,6 +43,7 @@ jest.mock('../utils/searchProvider', () => ({ getPostFilterColdSearchMaxQueueDepth: mockGetPostFilterColdSearchMaxQueueDepth, getPostFilterColdSearchMaxWaitMs: mockGetPostFilterColdSearchMaxWaitMs, getSearchRequestDeadlineMs: mockGetSearchRequestDeadlineMs, + isSearchProviderContractError: mockIsSearchProviderContractError, searchOpenSearch: mockSearchOpenSearch, shouldLogSearchPerformance: mockShouldLogSearchPerformance })); @@ -156,6 +158,7 @@ describe('Search controller SEARCH handler', () => { mockGetPostFilterColdSearchMaxQueueDepth.mockClear(); mockGetPostFilterColdSearchMaxWaitMs.mockClear(); mockGetSearchRequestDeadlineMs.mockClear(); + mockIsSearchProviderContractError.mockClear(); mockGetPostFilterCacheTtlMs.mockReturnValue(60000); mockGetPostFilterCacheMaxEntries.mockReturnValue(100); mockGetPostFilterColdSearchConcurrency.mockReturnValue(10); @@ -1331,6 +1334,7 @@ describe('Search controller SEARCH handler', () => { test.each([ ['ConnectionError', { name: 'ConnectionError' }], ['NoLivingConnectionsError', { name: 'NoLivingConnectionsError' }], + ['cve-core provider contract error', { code: 'SEARCH_PROVIDER_CONTRACT' }], ['OpenSearch 503 response', { name: 'ResponseError', meta: { statusCode: 503 } }] ])('SEARCH returns a retryable 503 when OpenSearch is unavailable through %s', async (_caseName, thrownError) => { mockApiSearch.mockRejectedValue(thrownError); diff --git a/routes/index.js b/routes/index.js index 787abaa..7cc0568 100644 --- a/routes/index.js +++ b/routes/index.js @@ -5,6 +5,8 @@ const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); const constants = require('../utils/constants'); const featureFlags = require('../utils/featureFlags'); +const searchDeadline = require('../utils/searchDeadline'); +const searchProvider = require('../utils/searchProvider'); const searchRateLimiter = require('../utils/searchRateLimiter'); const error = new errors.errors(); @@ -21,6 +23,30 @@ function healthCheck(req, res) { res.status(200).send(); } +async function readinessCheck(req, res) { + const lifecycle = searchDeadline.createSearchRequestLifecycle( + req, + res, + searchProvider.getOpenSearchReadinessTimeoutMs() + ); + + try { + await searchProvider.checkOpenSearchReadiness({ + signal: lifecycle.signal + }); + lifecycle.throwIfAborted(); + + return res.status(200).send(); + } catch (err) { + if (searchDeadline.isSearchClientDisconnectedError(err)) return; + + if (typeof res.set === 'function') res.set('Retry-After', '5'); + return res.status(503).json(error.searchNotReadyError()); + } finally { + lifecycle.complete(); + } +} + function parseError(req, res, next) { const err = validationResult(req).formatWith(({ location, msg, param, value, nestedErrors }) => { return msg; @@ -300,13 +326,13 @@ description: 'Search exceeded its OpenSearch or overall processing deadline', controller.SEARCH ); -// Endpoint used for AWS health checks +// Process liveness endpoint router.get( '/api/health-check', /* #swagger.operationId = 'healthCheck' -#swagger.summary = "Health check for AWS monitoring" -#swagger.description = "Health check for AWS monitoring" +#swagger.summary = "Process liveness check" +#swagger.description = "Returns 200 when the API process is running. This endpoint does not check OpenSearch." #swagger.responses[200] = { description: 'OK', headers: { @@ -322,6 +348,45 @@ headers: { healthCheck ); +// OpenSearch dependency readiness endpoint +router.get( + '/api/ready-check', + /* +#swagger.operationId = 'readinessCheck' +#swagger.summary = "OpenSearch readiness check" +#swagger.description = "Sends a minimal zero-result search to the configured CVE index. Returns 200 when the index is available with search permission, or a sanitized 503 response when configuration, connectivity, timeout, permission, or index availability checks fail." +#swagger.responses[200] = { +description: 'API is ready to serve search requests', +headers: { + "X-Request-ID": { + description: "Request correlation ID", + schema: { + type: "string" + } + } +} +} +#swagger.responses[503] = { +description: 'Search service is not ready', +headers: { + "Retry-After": { + description: "Seconds before retrying the readiness check", + schema: { + type: "integer" + } + }, + "X-Request-ID": { + description: "Request correlation ID", + schema: { + type: "string" + } + } +} +} +*/ + readinessCheck +); + router.post( '/webSearch', /* diff --git a/routes/index.test.js b/routes/index.test.js index dc12c74..c1cd095 100644 --- a/routes/index.test.js +++ b/routes/index.test.js @@ -24,11 +24,17 @@ jest.mock('../utils/featureFlags', () => ({ })); const mockLimitSearchRequests = jest.fn((req, res, next) => next()); +const mockCheckOpenSearchReadiness = jest.fn(); jest.mock('../utils/searchRateLimiter', () => ({ createSearchRateLimiter: jest.fn(() => mockLimitSearchRequests) })); +jest.mock('../utils/searchProvider', () => ({ + checkOpenSearchReadiness: mockCheckOpenSearchReadiness, + getOpenSearchReadinessTimeoutMs: jest.fn(() => 5000) +})); + const mw = require('../controllers/middleware'); const controller = require('../controllers/searchController'); const featureFlags = require('../utils/featureFlags'); @@ -42,6 +48,7 @@ describe('routes/index router', () => { jest.clearAllMocks(); featureFlags.isWebSearchEnabled.mockReturnValue(true); mockLimitSearchRequests.mockImplementation((req, res, next) => next()); + mockCheckOpenSearchReadiness.mockResolvedValue(true); }); test('GET /api/health-check returns 200', async () => { @@ -51,6 +58,35 @@ describe('routes/index router', () => { expect(response.status).toBe(200); expect(response.text).toBe(''); + expect(mockCheckOpenSearchReadiness).not.toHaveBeenCalled(); + }); + + test('GET /api/ready-check returns 200 when the configured OpenSearch index exists', async () => { + const response = await request(app, { + path: '/api/ready-check' + }); + + expect(response.status).toBe(200); + expect(response.text).toBe(''); + expect(mockCheckOpenSearchReadiness).toHaveBeenCalledWith({ + signal: expect.any(AbortSignal) + }); + }); + + test('GET /api/ready-check returns a sanitized 503 when OpenSearch is unavailable', async () => { + mockCheckOpenSearchReadiness.mockRejectedValue(new Error('sensitive internal OpenSearch details')); + + const response = await request(app, { + path: '/api/ready-check' + }); + + expect(response.status).toBe(503); + expect(response.headers['retry-after']).toBe('5'); + expect(response.body).toStrictEqual({ + error: 'SEARCH_NOT_READY', + message: 'Search service is not ready. Please try again later.' + }); + expect(response.text).not.toContain('sensitive internal OpenSearch details'); }); test('POST /search reaches middleware and controller for valid JSON body fields', async () => { diff --git a/utils/constants.js b/utils/constants.js index e282a28..e3f824e 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -24,6 +24,7 @@ const SEARCH_POST_FILTER_COLD_SEARCH_MAX_QUEUE_DEPTH = 500; const SEARCH_POST_FILTER_COLD_SEARCH_MAX_WAIT_MS = 10 * 1000; const SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS = 20 * 1000; const SEARCH_REQUEST_DEADLINE_MS = 30 * 1000; +const SEARCH_READINESS_TIMEOUT_MS = 5 * 1000; const rejectedCveStates = ['REJECT', 'REJECTED', 'Rejected']; const rejectedCveStateFields = ['cveMetadata.state', 'containers.cna.x_legacyV4Record.CVE_data_meta.STATE']; @@ -147,6 +148,7 @@ module.exports = { SEARCH_POST_FILTER_COLD_SEARCH_MAX_WAIT_MS, SEARCH_OPEN_SEARCH_REQUEST_TIMEOUT_MS, SEARCH_REQUEST_DEADLINE_MS, + SEARCH_READINESS_TIMEOUT_MS, RESULTS_PER_PAGE, REJECTED_CVE_STATES: rejectedCveStates, CPE_VERSION_RANGE_TYPES: cpeVersionRangeTypes, diff --git a/utils/errors.js b/utils/errors.js index d0ecd06..4c126e0 100644 --- a/utils/errors.js +++ b/utils/errors.js @@ -273,6 +273,10 @@ class errors { ); } + searchNotReadyError() { + return makeError('SEARCH_NOT_READY', 'Search service is not ready. Please try again later.'); + } + upstreamSearchError() { return makeError('UPSTREAM_SEARCH_ERROR', 'Search backend returned an error. Please try again later.'); } diff --git a/utils/errors.test.js b/utils/errors.test.js index f68c4f5..682af30 100644 --- a/utils/errors.test.js +++ b/utils/errors.test.js @@ -207,6 +207,14 @@ describe('error response helpers', () => { message: 'Too many search requests. Please retry later.' } ], + [ + 'searchNotReadyError', + [], + { + error: 'SEARCH_NOT_READY', + message: 'Search service is not ready. Please try again later.' + } + ], [ 'searchBodyError', [], diff --git a/utils/requestLogging.js b/utils/requestLogging.js index a363e83..57e7248 100644 --- a/utils/requestLogging.js +++ b/utils/requestLogging.js @@ -4,6 +4,7 @@ const { AppConfig } = require('cve-core'); const DEFAULT_SUCCESS_SAMPLE_RATE = 0.1; const DEFAULT_LOG_HEALTH_CHECKS = false; const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/; +const HEALTH_CHECK_PATHS = new Set(['/api/health-check', '/api/ready-check']); let requestLoggingConfig; @@ -74,7 +75,7 @@ function assignRequestId(req, res, next) { function shouldSkipAccessLog(req, res, random = Math.random) { if (res.statusCode >= 400) return false; - if (getRedactedRequestPath(req) === '/api/health-check' && !shouldLogHealthChecks()) { + if (HEALTH_CHECK_PATHS.has(getRedactedRequestPath(req)) && !shouldLogHealthChecks()) { return true; } diff --git a/utils/requestLogging.test.js b/utils/requestLogging.test.js index 9d90656..1cae11e 100644 --- a/utils/requestLogging.test.js +++ b/utils/requestLogging.test.js @@ -63,33 +63,39 @@ describe('requestLogging', () => { expect(next).toHaveBeenCalledTimes(1); }); - test('shouldSkipAccessLog suppresses successful health checks by default', () => { - expect( - requestLogging.shouldSkipAccessLog( - { - path: '/api/health-check' - }, - { - statusCode: 200 - }, - () => 0 - ) - ).toBe(true); - }); + test.each(['/api/health-check', '/api/ready-check'])( + 'shouldSkipAccessLog suppresses successful health check %s by default', + (path) => { + expect( + requestLogging.shouldSkipAccessLog( + { + path + }, + { + statusCode: 200 + }, + () => 0 + ) + ).toBe(true); + } + ); - test('shouldSkipAccessLog retains failed health checks', () => { - expect( - requestLogging.shouldSkipAccessLog( - { - path: '/api/health-check' - }, - { - statusCode: 500 - }, - () => 1 - ) - ).toBe(false); - }); + test.each(['/api/health-check', '/api/ready-check'])( + 'shouldSkipAccessLog retains failed health check %s', + (path) => { + expect( + requestLogging.shouldSkipAccessLog( + { + path + }, + { + statusCode: 500 + }, + () => 1 + ) + ).toBe(false); + } + ); test('shouldSkipAccessLog samples successful requests at the configured rate', () => { mockAppConfigHas.mockImplementation((path) => path === 'requestLogging.successSampleRate'); diff --git a/utils/searchProvider.js b/utils/searchProvider.js index 9b23133..929ec6b 100644 --- a/utils/searchProvider.js +++ b/utils/searchProvider.js @@ -2,6 +2,14 @@ const { AppConfig, BasicSearchManager, SearchProviderSpec, SearchReader } = requ const constants = require('./constants'); const searchDeadline = require('./searchDeadline'); +class SearchProviderContractError extends Error { + constructor() { + super('Installed cve-core SearchReader does not expose the required OpenSearch client contract.'); + this.name = 'SearchProviderContractError'; + this.code = 'SEARCH_PROVIDER_CONTRACT'; + } +} + let searchProviderConfig; let basicSearchManager; let searchReader; @@ -85,13 +93,27 @@ function getBasicSearchManager() { return basicSearchManager; } +function validateSearchReader(reader) { + const hasIndex = typeof reader?._cveIndex === 'string' && reader._cveIndex.trim() !== ''; + const hasSearchClient = typeof reader?._client?.search === 'function'; + + if (!hasIndex || !hasSearchClient) { + throw new SearchProviderContractError(); + } + + return reader; +} + function getSearchReader() { if (!searchReader) { const config = getSearchProviderConfig(); - searchReader = new SearchReader(config.providerEndpoint, config.index, { - ignore_ssl_cert_check_errors: config.allowUnknownSslCerts - }); - configureClientRequestTimeout(searchReader._client); + const reader = validateSearchReader( + new SearchReader(config.providerEndpoint, config.index, { + ignore_ssl_cert_check_errors: config.allowUnknownSslCerts + }) + ); + configureClientRequestTimeout(reader._client); + searchReader = reader; } return searchReader; @@ -154,31 +176,86 @@ function awaitAbortableRequest(request, signal) { }); } -async function searchOpenSearch(body, { signal, operation = 'search', onStart } = {}) { +async function executeOpenSearchRequest(createRequest, { signal, operation, onStart } = {}) { searchDeadline.throwIfAborted(signal); - const reader = getSearchReader(); const state = beginOpenSearchRequest(operation); try { onStart?.(state); - const request = reader._client.search( - { - index: reader._cveIndex, - body - }, - { - maxRetries: 0, - requestTimeout: getOpenSearchRequestTimeoutMs() - } - ); - const response = await awaitAbortableRequest(request, signal); + const response = await awaitAbortableRequest(createRequest(), signal); - return response.body; + return response; } finally { endOpenSearchRequest(operation); } } +async function searchOpenSearch(body, { signal, operation = 'search', onStart } = {}) { + searchDeadline.throwIfAborted(signal); + const reader = getSearchReader(); + const response = await executeOpenSearchRequest( + () => + reader._client.search( + { + index: reader._cveIndex, + body + }, + { + maxRetries: 0, + requestTimeout: getOpenSearchRequestTimeoutMs() + } + ), + { + signal, + operation, + onStart + } + ); + + return response.body; +} + +async function checkOpenSearchReadiness({ signal } = {}) { + searchDeadline.throwIfAborted(signal); + const reader = getSearchReader(); + await executeOpenSearchRequest( + () => + reader._client.search( + { + index: reader._cveIndex, + body: { + query: { + match_all: {} + }, + size: 0, + terminate_after: 1, + track_total_hits: false + } + }, + { + maxRetries: 0, + requestTimeout: getOpenSearchReadinessTimeoutMs() + } + ), + { + signal, + operation: 'readiness' + } + ); + + return true; +} + +function isSearchProviderContractError(err) { + return err instanceof SearchProviderContractError || err?.code === 'SEARCH_PROVIDER_CONTRACT'; +} + +function getOpenSearchReadinessTimeoutMs() { + return getIntegerConfig('search.readinessTimeoutMs', constants.SEARCH_READINESS_TIMEOUT_MS, { + min: 1 + }); +} + function shouldLogSearchPerformance() { if (!AppConfig.has('search.logPerformanceTimings')) return false; @@ -236,10 +313,12 @@ function resetSearchProviderForTests() { } module.exports = { + checkOpenSearchReadiness, getBasicSearchManager, getAllowUnknownSslCerts, getIntegerConfig, getOpenSearchRequestState, + getOpenSearchReadinessTimeoutMs, getPostFilterCacheMaxEntries, getPostFilterCacheTtlMs, getPostFilterColdSearchConcurrency, @@ -249,8 +328,11 @@ module.exports = { getSearchRequestDeadlineMs, getSearchProviderConfig, getSearchReader, + isSearchProviderContractError, normalizeBoolean, resetSearchProviderForTests, searchOpenSearch, - shouldLogSearchPerformance + shouldLogSearchPerformance, + validateSearchReader, + SearchProviderContractError }; diff --git a/utils/searchProvider.test.js b/utils/searchProvider.test.js index b6f7bbf..c817ad9 100644 --- a/utils/searchProvider.test.js +++ b/utils/searchProvider.test.js @@ -166,6 +166,98 @@ describe('searchProvider', () => { }); }); + test('checks OpenSearch readiness through the configured CVE index', async () => { + mockClientSearch.mockResolvedValue({ + body: { + hits: { + hits: [] + } + } + }); + + await expect(searchProvider.checkOpenSearchReadiness()).resolves.toBe(true); + + expect(mockClientSearch).toHaveBeenCalledWith( + { + index: 'test-index', + body: { + query: { + match_all: {} + }, + size: 0, + terminate_after: 1, + track_total_hits: false + } + }, + { + maxRetries: 0, + requestTimeout: 5000 + } + ); + expect(searchProvider.getOpenSearchRequestState()).toStrictEqual({ + activeCount: 0, + activeByOperation: {} + }); + }); + + test('fails readiness when the configured CVE index is unavailable', async () => { + const unavailable = new Error('index unavailable'); + mockClientSearch.mockRejectedValue(unavailable); + + await expect(searchProvider.checkOpenSearchReadiness()).rejects.toBe(unavailable); + }); + + test('aborts the readiness transport request when its lifecycle is canceled', async () => { + const pending = deferred(); + const request = pending.promise; + request.abort = jest.fn(); + mockClientSearch.mockReturnValue(request); + const controller = new AbortController(); + const readiness = searchProvider.checkOpenSearchReadiness({ + signal: controller.signal + }); + await Promise.resolve(); + + controller.abort(new searchDeadline.SearchDeadlineExceededError()); + + await expect(readiness).rejects.toMatchObject({ + code: 'SEARCH_TIMEOUT' + }); + expect(request.abort).toHaveBeenCalledTimes(1); + expect(searchProvider.getOpenSearchRequestState()).toStrictEqual({ + activeCount: 0, + activeByOperation: {} + }); + }); + + test('does not construct a SearchReader for an already-canceled readiness check', async () => { + const controller = new AbortController(); + controller.abort(new searchDeadline.SearchClientDisconnectedError()); + + await expect( + searchProvider.checkOpenSearchReadiness({ + signal: controller.signal + }) + ).rejects.toMatchObject({ + code: 'SEARCH_CLIENT_DISCONNECTED' + }); + expect(mockSearchReader).not.toHaveBeenCalled(); + expect(mockClientSearch).not.toHaveBeenCalled(); + }); + + test('rejects and does not cache an incompatible cve-core SearchReader', () => { + mockSearchReader.mockImplementationOnce(() => ({ + _cveIndex: 'test-index', + _client: { + transport: {} + } + })); + + expect(() => searchProvider.getSearchReader()).toThrow(searchProvider.SearchProviderContractError); + expect(searchProvider.getSearchReader()).toBeDefined(); + expect(mockSearchReader).toHaveBeenCalledTimes(2); + }); + test('aborts an active OpenSearch request when the lifecycle signal aborts', async () => { const pending = deferred(); const request = pending.promise; @@ -316,7 +408,8 @@ describe('searchProvider', () => { 'search.postFilterColdSearchMaxQueueDepth', 'search.postFilterColdSearchMaxWaitMs', 'search.openSearchRequestTimeoutMs', - 'search.requestDeadlineMs' + 'search.requestDeadlineMs', + 'search.readinessTimeoutMs' ].includes(key) ); mockAppConfigGet.mockImplementation((key) => { @@ -327,7 +420,8 @@ describe('searchProvider', () => { 'search.postFilterColdSearchMaxQueueDepth': '50', 'search.postFilterColdSearchMaxWaitMs': '15000', 'search.openSearchRequestTimeoutMs': '25000', - 'search.requestDeadlineMs': '45000' + 'search.requestDeadlineMs': '45000', + 'search.readinessTimeoutMs': '7000' }; return values[key]; }); @@ -339,6 +433,7 @@ describe('searchProvider', () => { expect(searchProvider.getPostFilterColdSearchMaxWaitMs()).toBe(15000); expect(searchProvider.getOpenSearchRequestTimeoutMs()).toBe(25000); expect(searchProvider.getSearchRequestDeadlineMs()).toBe(45000); + expect(searchProvider.getOpenSearchReadinessTimeoutMs()).toBe(7000); }); test('falls back to CPE post-filter runtime defaults when config values are invalid', () => { @@ -352,5 +447,6 @@ describe('searchProvider', () => { expect(searchProvider.getPostFilterColdSearchMaxWaitMs()).toBe(10000); expect(searchProvider.getOpenSearchRequestTimeoutMs()).toBe(20000); expect(searchProvider.getSearchRequestDeadlineMs()).toBe(30000); + expect(searchProvider.getOpenSearchReadinessTimeoutMs()).toBe(5000); }); }); From f6bc82728a845bfc9321468ffcb81a1b0d4adb6c Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 11:09:28 -0400 Subject: [PATCH 13/20] fix: align CPE NA range matching with NVD --- README.md | 2 + api-docs/openapi.json | 4 +- api-docs/openapi.test.js | 2 + controllers/searchController.test.js | 42 ++++++++++++++++ routes/swagger.js | 2 +- utils/cpe.js | 7 +++ utils/cpe.test.js | 75 ++++++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 21080c4..1de97db 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ curl -X POST "http://localhost:3000/search" \ The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. +The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. To match NVD `cpeName` behavior, numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This compatibility rule can produce different range behavior than a literal reading of source CVE Record applicability fields because NVD applies its own CPE enrichment when building CPE Match Criteria. + Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Criteria entry itself to be marked vulnerable. A vulnerable flag on an unrelated CPE entry does not satisfy the request. Omitting `isVulnerable`, or setting it to `false`, does not apply a vulnerability restriction. ### Virtual Match String Search diff --git a/api-docs/openapi.json b/api-docs/openapi.json index a860ab1..a2319e0 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", @@ -503,4 +503,4 @@ } } } -} \ No newline at end of file +} diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 3b205f1..388dda3 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -115,6 +115,8 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('CPE Match Criteria'); expect(cpeName.description).toContain('part, vendor, product, and version'); expect(cpeName.description).toContain('version range fields'); + expect(cpeName.description).toContain('logical value NA'); + expect(cpeName.description).toContain('numeric range boundaries'); expect(cpeName.description).toContain('HTTP 404'); expect(cpeName.description).toContain('virtualMatchString'); expect(cpeName.example).toBe('cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*'); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 1e5fe87..0e326c9 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -554,6 +554,48 @@ describe('Search controller SEARCH handler', () => { }); }); + test('applySearchPostFilters preserves NVD-compatible CPE NA matches from source record ranges', () => { + const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; + const candidates = [ + cpeHit('CVE-2020-17153', { + criteria, + vulnerable: true, + versionStartIncluding: '-' + }), + cpeHit('CVE-2024-38208', { + criteria, + vulnerable: true, + versionStartIncluding: '1.0.0', + versionEndExcluding: '128.0.2739.42' + }), + cpeHit('CVE-2026-35429', { + criteria, + vulnerable: true, + versionStartIncluding: '1.0.0', + versionEndExcluding: '148.0.3967.55' + }) + ]; + + const response = controller.applySearchPostFilters( + advancedResponse(candidates.length, candidates), + { + cpeName: 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + + expect(response.data.hits.total.value).toBe(3); + expect(response.data.hits.hits.map((hit) => hit._id)).toStrictEqual([ + 'CVE-2020-17153', + 'CVE-2024-38208', + 'CVE-2026-35429' + ]); + }); + test('SEARCH applies semantic virtualMatchString version range filtering before building response metadata', async () => { const applicable = cpeHit('CVE-2024-0001', { criteria: 'cpe:2.3:o:linux:linux_kernel:2.5:*:*:*:*:*:*:*' diff --git a/routes/swagger.js b/routes/swagger.js index 5a78b14..97b46bb 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, diff --git a/utils/cpe.js b/utils/cpe.js index 293daf8..1004343 100644 --- a/utils/cpe.js +++ b/utils/cpe.js @@ -1,5 +1,6 @@ const CPE_23_PREFIX = ['cpe', '2.3']; const CPE_COMPONENT_COUNT = 11; +const CPE_NOT_APPLICABLE = '-'; const REQUIRED_CPE_COMPONENT_INDEXES = [0, 1, 2, 3]; function parseCpe23(input) { @@ -336,6 +337,12 @@ function versionRangesOverlap(a, b) { } function rangeBoundaryMatches(assetVersion, cpeMatch) { + if (assetVersion === CPE_NOT_APPLICABLE) { + return ( + cpeMatch.versionStartExcluding !== CPE_NOT_APPLICABLE && cpeMatch.versionEndExcluding !== CPE_NOT_APPLICABLE + ); + } + if ( hasBoundary(cpeMatch.versionStartIncluding) && compareCpeVersions(assetVersion, cpeMatch.versionStartIncluding) < 0 diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 041728e..736fbfd 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -191,6 +191,81 @@ describe('CPE helpers', () => { ).toBe(false); }); + test('cpeMatchAppliesToCpeName treats the CPE NA version as nonnumeric for NVD-compatible ranges', () => { + const cpeName = 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*'; + const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; + + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria, + vulnerable: true, + versionStartIncluding: '-' + }, + cpeName, + undefined, + { requireVulnerable: true } + ) + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria, + vulnerable: true, + versionStartIncluding: '1.0.0', + versionEndExcluding: '128.0.2739.42' + }, + cpeName, + undefined, + { requireVulnerable: true } + ) + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria, + vulnerable: true, + versionStartIncluding: '1.0.0', + versionEndExcluding: '148.0.3967.55' + }, + cpeName, + undefined, + { requireVulnerable: true } + ) + ).toBe(true); + }); + + test('cpeMatchAppliesToCpeName still honors exact and exclusive CPE NA criteria', () => { + const cpeName = 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*'; + + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:microsoft:edge:1.0.0:*:*:*:*:android:*:*' + }, + cpeName + ) + ).toBe(false); + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*', + versionStartExcluding: '-' + }, + cpeName + ) + ).toBe(false); + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*', + versionEndExcluding: '-' + }, + cpeName + ) + ).toBe(false); + }); + test('cpeMatchAppliesToCpeName requires vulnerability on the matching entry when requested', () => { const cpeName = 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*'; const matchingCriteria = 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*'; From 85232593b2dee717257dc35e0fa5483f53bff12e Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 11:27:31 -0400 Subject: [PATCH 14/20] fix: order CPE version qualifiers semantically --- README.md | 2 + api-docs/openapi.json | 6 +-- api-docs/openapi.test.js | 5 +++ controllers/middleware.test.js | 27 ++++++++++++ routes/swagger.js | 6 +-- utils/cpe.js | 50 +++++++++++++++++++--- utils/cpe.test.js | 78 ++++++++++++++++++++++++++++++++++ 7 files changed, 163 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 1de97db..761b7cf 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,8 @@ curl -X POST "http://localhost:3000/search" \ Version ranges use overlap semantics. A stored wildcard-version CPE Match Criteria entry such as `cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*` with `versionEndExcluding=6.9` matches requested ranges that overlap versions before `6.9`, and does not match a requested range that starts at `6.9`. +CPE version comparisons used by both `cpeName` applicability and `virtualMatchString` version ranges use Maven-style qualifier precedence: `alpha`/`a` < `beta`/`b` < `milestone`/`m` < `rc`/`cr` < `snapshot` < an unqualified release/`ga`/`final`/`release` < `sp` < unknown qualifiers. Unknown qualifiers, including `hf`, sort after known qualifiers in case-insensitive lexical order. This means a range beginning at `1.0.0-rc1` includes `1.0.0`, while a range ending before `1.0.0` can include prerelease versions. + Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. That would intentionally diverge from NVD behavior and should be handled as a policy decision before implementation. `POST /webSearch` is deprecated in favor of `POST /search`. It remains enabled by default during consumer migration and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index a2319e0..19a5415 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", @@ -469,7 +469,7 @@ "type": "string", "maxLength": 256, "example": "2.2", - "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." + "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionStartType": { "type": "string", @@ -483,7 +483,7 @@ "type": "string", "maxLength": 256, "example": "2.6", - "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component." + "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionEndType": { "type": "string", diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 388dda3..af588a3 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -72,13 +72,18 @@ describe('OpenAPI contract', () => { expect(schema.properties.noRejected.description).toContain('When true'); expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry'); expect(schema.properties.isVulnerable.description).toContain('False or omitted'); + expect(schema.properties.cpeName.description).toContain('Maven-style ordering'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); expect(schema.properties.cveId).toBeUndefined(); expect(schema.properties.versionStart.description).toContain('versionStartType'); expect(schema.properties.versionStart.description).toContain('less than versionEnd'); + expect(schema.properties.versionStart.description).toContain('Maven-style ordering'); expect(schema.properties.versionEnd.description).toContain('versionEndType'); + expect(schema.properties.versionEnd.description).toContain( + 'prerelease qualifiers before an unqualified release' + ); expect(schema.properties.virtualMatchString.description).toContain('CPE Match Criteria'); expect(schema.properties.virtualMatchString.description).toContain('compatible'); expect(schema.properties.virtualMatchString.description).toContain('without a version component'); diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index b036f2f..d86a6ce 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -226,6 +226,33 @@ describe('middleware validation function', () => { expectValidationSuccess(); }); + test('ValidateParams accepts a prerelease-to-release version range', () => { + validate({ + virtualMatchString: 'cpe:2.3:a:vendor:product', + versionStart: '1.0.0-rc1', + versionStartType: 'including', + versionEnd: '1.0.0', + versionEndType: 'including' + }); + + expectValidationSuccess(); + }); + + test('ValidateParams rejects a release-to-prerelease version range', () => { + validate({ + virtualMatchString: 'cpe:2.3:a:vendor:product', + versionStart: '1.0.0', + versionStartType: 'including', + versionEnd: '1.0.0-rc1', + versionEndType: 'including' + }); + + expectValidationError( + 'VERSION_RANGE_ORDER', + 'The versionStart parameter must be less than versionEnd. Equal values require both bounds to be including.' + ); + }); + test('ValidateParams returns an error when version range params are used without virtualMatchString', () => { validate({ versionStart: '2.2', diff --git a/routes/swagger.js b/routes/swagger.js index 97b46bb..dad6758 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, @@ -193,7 +193,7 @@ const searchRequestProperties = { } ), versionStart: searchRequestProperty( - 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', + 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionStart, @@ -208,7 +208,7 @@ const searchRequestProperties = { } ), versionEnd: searchRequestProperty( - 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. virtualMatchString must not include a version component.', + 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionEnd, diff --git a/utils/cpe.js b/utils/cpe.js index 1004343..008be28 100644 --- a/utils/cpe.js +++ b/utils/cpe.js @@ -2,6 +2,16 @@ const CPE_23_PREFIX = ['cpe', '2.3']; const CPE_COMPONENT_COUNT = 11; const CPE_NOT_APPLICABLE = '-'; const REQUIRED_CPE_COMPONENT_INDEXES = [0, 1, 2, 3]; +const VERSION_QUALIFIERS = ['alpha', 'beta', 'milestone', 'rc', 'snapshot', '', 'sp']; +const VERSION_QUALIFIER_ALIASES = { + a: 'alpha', + b: 'beta', + cr: 'rc', + final: '', + ga: '', + m: 'milestone', + release: '' +}; function parseCpe23(input) { const value = String(input ?? '') @@ -232,6 +242,33 @@ function compareNumericTokens(a, b) { return normalizedA > normalizedB ? 1 : -1; } +function normalizeVersionQualifier(token) { + return Object.prototype.hasOwnProperty.call(VERSION_QUALIFIER_ALIASES, token) + ? VERSION_QUALIFIER_ALIASES[token] + : token; +} + +function compareVersionQualifiers(a, b) { + const normalizedA = normalizeVersionQualifier(a); + const normalizedB = normalizeVersionQualifier(b); + const rankA = VERSION_QUALIFIERS.indexOf(normalizedA); + const rankB = VERSION_QUALIFIERS.indexOf(normalizedB); + + if (rankA !== -1 || rankB !== -1) { + if (rankA === -1) return 1; + if (rankB === -1) return -1; + return Math.sign(rankA - rankB); + } + + if (normalizedA === normalizedB) return 0; + return normalizedA > normalizedB ? 1 : -1; +} + +function compareVersionTokenToRelease(token) { + if (/^\d+$/.test(token)) return /^0+$/.test(token) ? 0 : 1; + return compareVersionQualifiers(token, ''); +} + function compareCpeVersions(a, b) { const tokensA = tokenizeVersion(a); const tokensB = tokenizeVersion(b); @@ -246,12 +283,14 @@ function compareCpeVersions(a, b) { const tokenB = tokensB[i]; if (tokenA === undefined) { - if (/^0+$/.test(tokenB)) continue; - return -1; + const result = compareVersionTokenToRelease(tokenB); + if (result === 0) continue; + return -result; } if (tokenB === undefined) { - if (/^0+$/.test(tokenA)) continue; - return 1; + const result = compareVersionTokenToRelease(tokenA); + if (result === 0) continue; + return result; } if (tokenA === tokenB) continue; @@ -268,7 +307,8 @@ function compareCpeVersions(a, b) { return aIsNumber ? 1 : -1; } - return tokenA > tokenB ? 1 : -1; + const result = compareVersionQualifiers(tokenA, tokenB); + if (result !== 0) return result; } return 0; diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 736fbfd..d12a6b0 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -166,6 +166,37 @@ describe('CPE helpers', () => { expect(cpe.compareCpeVersions('1.0', '1.0.1')).toBeLessThan(0); }); + test('compareCpeVersions orders Maven-style qualifiers around an unqualified release', () => { + const orderedVersions = [ + '1.0.0-alpha1', + '1.0.0-alpha2', + '1.0.0-beta1', + '1.0.0-milestone1', + '1.0.0-rc1', + '1.0.0-snapshot', + '1.0.0', + '1.0.0-sp1', + '1.0.0-hf1', + '1.0.0-zz1' + ]; + + for (let index = 1; index < orderedVersions.length; index++) { + expect(cpe.compareCpeVersions(orderedVersions[index - 1], orderedVersions[index])).toBeLessThan(0); + } + }); + + test.each([ + ['1.0.0-a1', '1.0.0-alpha1'], + ['1.0.0-b1', '1.0.0-beta1'], + ['1.0.0-m1', '1.0.0-milestone1'], + ['1.0.0-cr1', '1.0.0-rc1'], + ['1.0.0-GA', '1.0.0'], + ['1.0.0-final', '1.0.0'], + ['1.0.0-release', '1.0.0'] + ])('compareCpeVersions treats %s and %s as equivalent qualifier forms', (left, right) => { + expect(cpe.compareCpeVersions(left, right)).toBe(0); + }); + test('cpeMatchAppliesToCpeName honors inclusive and exclusive version boundaries', () => { const cpeName = 'cpe:2.3:a:vendor:product:2.10:*:*:*:*:*:*:*'; @@ -191,6 +222,19 @@ describe('CPE helpers', () => { ).toBe(false); }); + test('cpeMatchAppliesToCpeName treats prerelease qualifiers as earlier than the unqualified release', () => { + const criteria = { + criteria: 'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*', + versionStartIncluding: '1.0.0-rc1', + versionEndExcluding: '1.0.0-sp1' + }; + + expect(cpe.cpeMatchAppliesToCpeName(criteria, 'cpe:2.3:a:vendor:product:1.0.0:*:*:*:*:*:*:*')).toBe(true); + expect(cpe.cpeMatchAppliesToCpeName(criteria, 'cpe:2.3:a:vendor:product:1.0.0-beta1:*:*:*:*:*:*:*')).toBe( + false + ); + }); + test('cpeMatchAppliesToCpeName treats the CPE NA version as nonnumeric for NVD-compatible ranges', () => { const cpeName = 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*'; const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; @@ -348,6 +392,40 @@ describe('CPE helpers', () => { ).toBe(false); }); + test('cpeMatchOverlapsVirtualMatchStringRange applies qualifier precedence to range overlap', () => { + const matchString = cpe.parseVirtualMatchString('cpe:2.3:a:vendor:product'); + + expect( + cpe.cpeMatchOverlapsVirtualMatchStringRange( + { + criteria: 'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*', + versionEndExcluding: '1.0.0' + }, + { + matchString, + versionStart: '1.0.0-rc1', + versionStartType: 'including', + versionEnd: '1.0.0', + versionEndType: 'including' + } + ) + ).toBe(true); + + expect( + cpe.cpeMatchOverlapsVirtualMatchStringRange( + { + criteria: 'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*', + versionEndExcluding: '1.0.0-rc1' + }, + { + matchString, + versionStart: '1.0.0', + versionStartType: 'including' + } + ) + ).toBe(false); + }); + test('cpeMatchOverlapsVirtualMatchStringRange matches upper-bounded wildcard criteria before the excluded boundary', () => { const cpeMatch = { criteria: 'cpe:2.3:o:linux:linux_kernel:*:*:*:*:*:*:*:*', From 75d897e1fa8936a5533acee9f45b2519c41c7296 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 11:42:30 -0400 Subject: [PATCH 15/20] fix: correlate all CPE name components --- README.md | 2 +- api-docs/openapi.json | 2 +- api-docs/openapi.test.js | 4 +++ controllers/searchController.test.js | 44 ++++++++++++++++++++++++++ routes/swagger.js | 2 +- utils/cpe.js | 6 ++-- utils/cpe.test.js | 47 ++++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 761b7cf..4446849 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ curl -X POST "http://localhost:3000/search" \ -d '{"cpeName":"cpe:2.3:a:freetype:freetype:*:*:*:*:*:*:*:*"}' ``` -The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. +The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Every concrete non-version component in the input, such as edition, language, target software, or target hardware, must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks. Optional input components set to `*` remain broad matches. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. To match NVD `cpeName` behavior, numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This compatibility rule can produce different range behavior than a literal reading of source CVE Record applicability fields because NVD applies its own CPE enrichment when building CPE Match Criteria. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 19a5415..f635444 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index af588a3..a71b4d1 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -73,6 +73,10 @@ describe('OpenAPI contract', () => { expect(schema.properties.isVulnerable.description).toContain('same CPE Match Criteria entry'); expect(schema.properties.isVulnerable.description).toContain('False or omitted'); expect(schema.properties.cpeName.description).toContain('Maven-style ordering'); + expect(schema.properties.cpeName.description).toContain( + 'Every concrete non-version component must be compatible with the same CPE Match Criteria entry' + ); + expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 0e326c9..aac0d50 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -554,6 +554,50 @@ describe('Search controller SEARCH handler', () => { }); }); + test('applySearchPostFilters does not combine platform and version matches from different CPE entries', () => { + const candidate = cpeHit('CVE-2026-0250', { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:macos:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.0', + versionEndExcluding: '6.1' + }); + candidate._source.containers.cna.cpeApplicability.nodes[0].cpeMatch.push( + { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:macos:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.2', + versionEndExcluding: '6.2.7' + }, + { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:android:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.1', + versionEndExcluding: '6.1.13' + }, + { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:chrome_os:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.1', + versionEndExcluding: '6.1.13' + } + ); + + const response = controller.applySearchPostFilters( + advancedResponse(1, [candidate]), + { + cpeName: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.1.1:*:macos:*:*:*:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + + expect(response.data.hits.total.value).toBe(0); + expect(response.data.hits.hits).toStrictEqual([]); + }); + test('applySearchPostFilters preserves NVD-compatible CPE NA matches from source record ranges', () => { const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; const candidates = [ diff --git a/routes/swagger.js b/routes/swagger.js index dad6758..0c4f54f 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, diff --git a/utils/cpe.js b/utils/cpe.js index 008be28..fdecb49 100644 --- a/utils/cpe.js +++ b/utils/cpe.js @@ -315,7 +315,7 @@ function compareCpeVersions(a, b) { } function componentMatches(assetComponent, criteriaComponent) { - return criteriaComponent === '*' || criteriaComponent === assetComponent; + return assetComponent === '*' || criteriaComponent === '*' || criteriaComponent === assetComponent; } function hasBoundary(value) { @@ -421,7 +421,9 @@ function cpeMatchAppliesToCpeName(cpeMatch, cpeName, context, { requireVulnerabl if (!asset.valid || !criteria.valid) return false; if (requireVulnerable && cpeMatch?.vulnerable !== true) return false; - for (const index of [0, 1, 2]) { + for (let index = 0; index < asset.components.length; index++) { + if (index === 3) continue; + if (!componentMatches(asset.components[index], criteria.components[index])) { return false; } diff --git a/utils/cpe.test.js b/utils/cpe.test.js index d12a6b0..612a3fd 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -235,6 +235,53 @@ describe('CPE helpers', () => { ); }); + test('cpeMatchAppliesToCpeName requires every concrete non-version component to match', () => { + const cpeName = 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.1.1:*:macos:*:*:*:*:*'; + const androidRange = { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:android:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.1', + versionEndExcluding: '6.1.13' + }; + const macosRange = { + ...androidRange, + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:macos:*:*:*:*:*' + }; + + expect(cpe.cpeMatchAppliesToCpeName(androidRange, cpeName, undefined, { requireVulnerable: true })).toBe(false); + expect(cpe.cpeMatchAppliesToCpeName(macosRange, cpeName, undefined, { requireVulnerable: true })).toBe(true); + }); + + test('cpeMatchAppliesToCpeName rejects an exact version criterion with a different platform component', () => { + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.1.1:*:android:*:*:*:*:*', + vulnerable: true + }, + 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.1.1:*:macos:*:*:*:*:*', + undefined, + { requireVulnerable: true } + ) + ).toBe(false); + }); + + test('cpeMatchAppliesToCpeName keeps wildcard optional input components broad', () => { + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:android:*:*:*:*:*', + vulnerable: true, + versionStartIncluding: '6.1', + versionEndExcluding: '6.1.13' + }, + 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.1.1:*:*:*:*:*:*:*', + undefined, + { requireVulnerable: true } + ) + ).toBe(true); + }); + test('cpeMatchAppliesToCpeName treats the CPE NA version as nonnumeric for NVD-compatible ranges', () => { const cpeName = 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*'; const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; From ab6f0c232ca28341a9726a2942f520f79eeab05a Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 11:57:02 -0400 Subject: [PATCH 16/20] fix: make CPE literal matching case-insensitive --- README.md | 4 +-- api-docs/openapi.json | 4 +-- api-docs/openapi.test.js | 3 ++ controllers/searchController.test.js | 36 ++++++++++++++++--- routes/swagger.js | 4 +-- utils/cpe.js | 16 ++++----- utils/cpe.test.js | 53 ++++++++++++++++++++++++++++ utils/searchQueryBuilder.js | 6 ++-- utils/searchQueryBuilder.test.js | 6 ++-- 9 files changed, 110 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 4446849..9416e67 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ curl -X POST "http://localhost:3000/search" \ -d '{"cpeName":"cpe:2.3:a:freetype:freetype:*:*:*:*:*:*:*:*"}' ``` -The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. Every concrete non-version component in the input, such as edition, language, target software, or target hardware, must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks. Optional input components set to `*` remain broad matches. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. +The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. In accordance with the CPE 2.3 Name Matching specification, CPE string literal comparisons are insensitive to lexical case, so an input component such as `macos` matches stored criteria containing `macOS`. Every concrete non-version component in the input, such as edition, language, target software, or target hardware, must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks. Optional input components set to `*` remain broad matches. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. To match NVD `cpeName` behavior, numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This compatibility rule can produce different range behavior than a literal reading of source CVE Record applicability fields because NVD applies its own CPE enrichment when building CPE Match Criteria. @@ -158,7 +158,7 @@ Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Cr ### Virtual Match String Search -Use `virtualMatchString` when a user needs NVD-style CPE Match Criteria compatibility matching instead of concrete asset matching. Omitted trailing components and `*` components act as broad matches, matching NVD's broad CPE Match String behavior. +Use `virtualMatchString` when a user needs NVD-style CPE Match Criteria compatibility matching instead of concrete asset matching. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches, matching NVD's broad CPE Match String behavior. Partial product example: diff --git a/api-docs/openapi.json b/api-docs/openapi.json index f635444..23fe2da 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", @@ -497,7 +497,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:o:microsoft:windows_10_1809", - "description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component." + "description": "Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. CPE string literal comparisons are insensitive to lexical case. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component." } } } diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index a71b4d1..26884eb 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -77,6 +77,7 @@ describe('OpenAPI contract', () => { 'Every concrete non-version component must be compatible with the same CPE Match Criteria entry' ); expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches'); + expect(schema.properties.cpeName.description).toContain('insensitive to lexical case'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); @@ -90,6 +91,7 @@ describe('OpenAPI contract', () => { ); expect(schema.properties.virtualMatchString.description).toContain('CPE Match Criteria'); expect(schema.properties.virtualMatchString.description).toContain('compatible'); + expect(schema.properties.virtualMatchString.description).toContain('insensitive to lexical case'); expect(schema.properties.virtualMatchString.description).toContain('without a version component'); expect(schema.properties.virtualMatchString.example).toBe('cpe:2.3:o:microsoft:windows_10_1809'); expect(Object.keys(mediaType.examples)).toStrictEqual([ @@ -124,6 +126,7 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('CPE Match Criteria'); expect(cpeName.description).toContain('part, vendor, product, and version'); expect(cpeName.description).toContain('version range fields'); + expect(cpeName.description).toContain('insensitive to lexical case'); expect(cpeName.description).toContain('logical value NA'); expect(cpeName.description).toContain('numeric range boundaries'); expect(cpeName.description).toContain('HTTP 404'); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index aac0d50..440fa66 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -381,7 +381,8 @@ describe('Search controller SEARCH handler', () => { ].map((value) => ({ prefix: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value + value, + case_insensitive: true } } })), @@ -431,7 +432,8 @@ describe('Search controller SEARCH handler', () => { ].map((value) => ({ prefix: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value + value, + case_insensitive: true } } })), @@ -598,6 +600,30 @@ describe('Search controller SEARCH handler', () => { expect(response.data.hits.hits).toStrictEqual([]); }); + test('applySearchPostFilters matches CPE string literals without lexical case', () => { + const candidate = cpeHit('CVE-2026-0251', { + criteria: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:*:*:*:macOS:*:*', + vulnerable: true, + versionStartIncluding: '6.0', + versionEndExcluding: '6.1' + }); + + const response = controller.applySearchPostFilters( + advancedResponse(1, [candidate]), + { + cpeName: 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.0.1:*:*:*:*:macos:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + + expect(response.data.hits.total.value).toBe(1); + expect(response.data.hits.hits).toStrictEqual([candidate]); + }); + test('applySearchPostFilters preserves NVD-compatible CPE NA matches from source record ranges', () => { const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; const candidates = [ @@ -1336,7 +1362,8 @@ describe('Search controller SEARCH handler', () => { ].map((value) => ({ prefix: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value + value, + case_insensitive: true } } })), @@ -1364,7 +1391,8 @@ describe('Search controller SEARCH handler', () => { { regexp: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value: 'cpe:2\\.3:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*' + value: 'cpe:2\\.3:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*', + case_insensitive: true } } } diff --git a/routes/swagger.js b/routes/swagger.js index 0c4f54f..4fa72d5 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, @@ -223,7 +223,7 @@ const searchRequestProperties = { } ), virtualMatchString: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component.', + 'Returns CVE records whose CPE Match Criteria are compatible with the provided CPE 2.3 match string. CPE string literal comparisons are insensitive to lexical case. Omitted trailing components and * components act as broad matches. When used with versionStart or versionEnd, provide a partial CPE 2.3 match string without a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.virtualMatchString, diff --git a/utils/cpe.js b/utils/cpe.js index fdecb49..7301566 100644 --- a/utils/cpe.js +++ b/utils/cpe.js @@ -314,8 +314,12 @@ function compareCpeVersions(a, b) { return 0; } +function cpeLiteralsEqual(left, right) { + return String(left).toLowerCase() === String(right).toLowerCase(); +} + function componentMatches(assetComponent, criteriaComponent) { - return assetComponent === '*' || criteriaComponent === '*' || criteriaComponent === assetComponent; + return assetComponent === '*' || criteriaComponent === '*' || cpeLiteralsEqual(criteriaComponent, assetComponent); } function hasBoundary(value) { @@ -430,16 +434,12 @@ function cpeMatchAppliesToCpeName(cpeMatch, cpeName, context, { requireVulnerabl } if (criteria.version !== '*') { - return criteria.version === asset.version; + return cpeLiteralsEqual(criteria.version, asset.version); } return rangeBoundaryMatches(asset.version, cpeMatch); } -function componentMatchesVirtualMatchString(matchComponent, criteriaComponent) { - return matchComponent === '*' || criteriaComponent === '*' || criteriaComponent === matchComponent; -} - function cpeMatchOverlapsVirtualMatchStringRange(cpeMatch, virtualMatchStringRange, context) { const matchString = virtualMatchStringRange?.matchString?.valid ? virtualMatchStringRange.matchString @@ -451,7 +451,7 @@ function cpeMatchOverlapsVirtualMatchStringRange(cpeMatch, virtualMatchStringRan for (let index = 0; index < matchString.components.length; index++) { if (index === 3) continue; - if (!componentMatchesVirtualMatchString(matchString.components[index], criteria.components[index])) { + if (!componentMatches(matchString.components[index], criteria.components[index])) { return false; } } @@ -474,7 +474,7 @@ function cpeMatchMatchesVirtualMatchString(cpeMatch, virtualMatchString, context if (!matchString.valid || !criteria.valid) return false; for (let index = 0; index < matchString.components.length; index++) { - if (!componentMatchesVirtualMatchString(matchString.components[index], criteria.components[index])) { + if (!componentMatches(matchString.components[index], criteria.components[index])) { return false; } } diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 612a3fd..8fcee19 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -252,6 +252,35 @@ describe('CPE helpers', () => { expect(cpe.cpeMatchAppliesToCpeName(macosRange, cpeName, undefined, { requireVulnerable: true })).toBe(true); }); + test('cpeMatchAppliesToCpeName compares concrete string literals without lexical case', () => { + const cpeMatch = { + criteria: 'cpe:2.3:a:Palo_Alto_Networks:GlobalProtect_App:*:*:*:*:*:macOS:*:*', + vulnerable: true, + versionStartIncluding: '6.0', + versionEndExcluding: '6.1' + }; + + expect( + cpe.cpeMatchAppliesToCpeName( + cpeMatch, + 'cpe:2.3:a:palo_alto_networks:globalprotect_app:6.0.1:*:*:*:*:macos:*:*', + undefined, + { requireVulnerable: true } + ) + ).toBe(true); + }); + + test('cpeMatchAppliesToCpeName compares exact criteria versions without lexical case', () => { + expect( + cpe.cpeMatchAppliesToCpeName( + { + criteria: 'cpe:2.3:a:vendor:product:1.0.0-RC1:*:*:*:*:*:*:*' + }, + 'cpe:2.3:a:vendor:product:1.0.0-rc1:*:*:*:*:*:*:*' + ) + ).toBe(true); + }); + test('cpeMatchAppliesToCpeName rejects an exact version criterion with a different platform component', () => { expect( cpe.cpeMatchAppliesToCpeName( @@ -519,6 +548,30 @@ describe('CPE helpers', () => { expect(cpe.cpeMatchMatchesVirtualMatchString(windowsX86, 'cpe:2.3:o:microsoft:windows_11_23h2')).toBe(false); }); + test('virtualMatchString comparisons are insensitive to lexical case', () => { + const mixedCaseCriteria = { + criteria: 'cpe:2.3:a:Palo_Alto_Networks:GlobalProtect_App:*:*:*:*:*:macOS:*:*', + versionStartIncluding: '6.0', + versionEndExcluding: '6.1' + }; + + expect( + cpe.cpeMatchMatchesVirtualMatchString( + mixedCaseCriteria, + 'cpe:2.3:a:palo_alto_networks:globalprotect_app:*:*:*:*:*:macos:*:*' + ) + ).toBe(true); + expect( + cpe.cpeMatchOverlapsVirtualMatchStringRange(mixedCaseCriteria, { + matchString: cpe.parseVirtualMatchString('cpe:2.3:a:palo_alto_networks:globalprotect_app'), + versionStart: '6.0.1', + versionStartType: 'including', + versionEnd: '6.0.2', + versionEndType: 'excluding' + }) + ).toBe(true); + }); + test('hitAppliesToCpeName searches nested cpeApplicability nodes', () => { const hit = { _source: { diff --git a/utils/searchQueryBuilder.js b/utils/searchQueryBuilder.js index 83104ce..07894bb 100644 --- a/utils/searchQueryBuilder.js +++ b/utils/searchQueryBuilder.js @@ -449,7 +449,8 @@ function buildCpeCriteriaPrefixFilter(field, prefixes) { const prefixFilters = prefixes.map((value) => ({ prefix: { [field]: { - value + value, + case_insensitive: true } } })); @@ -469,7 +470,8 @@ function buildCpeCriteriaRegexpFilter(field, value) { return { regexp: { [field]: { - value + value, + case_insensitive: true } } }; diff --git a/utils/searchQueryBuilder.test.js b/utils/searchQueryBuilder.test.js index d8f9e31..9430664 100644 --- a/utils/searchQueryBuilder.test.js +++ b/utils/searchQueryBuilder.test.js @@ -4,7 +4,8 @@ function prefixFilter(value) { return { prefix: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value + value, + case_insensitive: true } } }; @@ -138,7 +139,8 @@ describe('searchQueryBuilder buildSearchRequest()', () => { { regexp: { 'containers.cna.cpeApplicability.nodes.cpeMatch.criteria.keyword': { - value: 'cpe:2\\.3:(o|\\*):(microsoft|\\*):(windows_10_1809|\\*):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:(x86|\\*):[^:]*' + value: 'cpe:2\\.3:(o|\\*):(microsoft|\\*):(windows_10_1809|\\*):[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:[^:]*:(x86|\\*):[^:]*', + case_insensitive: true } } } From 1fa490c3c9ab2220dbbbf6855073864a034a193c Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Tue, 28 Jul 2026 12:06:25 -0400 Subject: [PATCH 17/20] test: lock down NVD leading-zero version behavior --- README.md | 2 ++ api-docs/openapi.json | 6 ++-- api-docs/openapi.test.js | 7 ++++ controllers/searchController.test.js | 50 ++++++++++++++++++++++++++++ routes/swagger.js | 6 ++-- utils/cpe.test.js | 42 ++++++++++++++++++++++- 6 files changed, 106 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9416e67..f7605a6 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,8 @@ Version ranges use overlap semantics. A stored wildcard-version CPE Match Criter CPE version comparisons used by both `cpeName` applicability and `virtualMatchString` version ranges use Maven-style qualifier precedence: `alpha`/`a` < `beta`/`b` < `milestone`/`m` < `rc`/`cr` < `snapshot` < an unqualified release/`ga`/`final`/`release` < `sp` < unknown qualifiers. Unknown qualifiers, including `hf`, sort after known qualifiers in case-insensitive lexical order. This means a range beginning at `1.0.0-rc1` includes `1.0.0`, while a range ending before `1.0.0` can include prerelease versions. +As observed in the NVD CVE API on July 28, 2026, numeric segments used in version range comparisons ignore leading zeros. A stored exclusive upper bound of `10.05.1` therefore includes `10.5.0`, and CVE-2026-33280's exclusive `6.02` upper bound includes `6.1` but excludes `6.2`. This normalization applies only to range comparison. Exact CPE Match Criteria preserve leading zeros, so the CVE-2025-9392 versions `1.0.04.001` and `1.0.4.1` are not equivalent. This NVD compatibility behavior is not a universal firmware-versioning rule and can differ from a vendor's actual release sequence. + Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. That would intentionally diverge from NVD behavior and should be handled as a policy decision before implementation. `POST /webSearch` is deprecated in favor of `POST /search`. It remains enabled by default during consumer migration and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 23fe2da..00cc2d7 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", @@ -469,7 +469,7 @@ "type": "string", "maxLength": 256, "example": "2.2", - "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." + "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionStartType": { "type": "string", @@ -483,7 +483,7 @@ "type": "string", "maxLength": 256, "example": "2.6", - "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." + "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionEndType": { "type": "string", diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 26884eb..d5216d1 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -78,14 +78,19 @@ describe('OpenAPI contract', () => { ); expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches'); expect(schema.properties.cpeName.description).toContain('insensitive to lexical case'); + expect(schema.properties.cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); + expect(schema.properties.cpeName.description).toContain('July 28, 2026'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); expect(schema.properties.cveId).toBeUndefined(); expect(schema.properties.versionStart.description).toContain('versionStartType'); expect(schema.properties.versionStart.description).toContain('less than versionEnd'); + expect(schema.properties.versionStart.description).toContain('ranges ignore leading zeros'); + expect(schema.properties.versionStart.description).toContain('July 28, 2026'); expect(schema.properties.versionStart.description).toContain('Maven-style ordering'); expect(schema.properties.versionEnd.description).toContain('versionEndType'); + expect(schema.properties.versionEnd.description).toContain('exact CPE Match Criteria preserve leading zeros'); expect(schema.properties.versionEnd.description).toContain( 'prerelease qualifiers before an unqualified release' ); @@ -127,6 +132,8 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('part, vendor, product, and version'); expect(cpeName.description).toContain('version range fields'); expect(cpeName.description).toContain('insensitive to lexical case'); + expect(cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); + expect(cpeName.description).toContain('July 28, 2026'); expect(cpeName.description).toContain('logical value NA'); expect(cpeName.description).toContain('numeric range boundaries'); expect(cpeName.description).toContain('HTTP 404'); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 440fa66..4c5bebd 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -624,6 +624,56 @@ describe('Search controller SEARCH handler', () => { expect(response.data.hits.hits).toStrictEqual([candidate]); }); + test('applySearchPostFilters preserves exact version padding while normalizing numeric range segments', () => { + const exactCandidate = cpeHit('CVE-2025-9392', { + criteria: 'cpe:2.3:o:linksys:re6250_firmware:1.0.04.001:*:*:*:*:*:*:*', + vulnerable: true + }); + const rangeCandidate = cpeHit('CVE-2026-33280', { + criteria: 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:*:*:*:*:*:*:*:*', + vulnerable: true, + versionEndExcluding: '6.02' + }); + + const exactResponse = controller.applySearchPostFilters( + advancedResponse(1, [exactCandidate]), + { + cpeName: 'cpe:2.3:o:linksys:re6250_firmware:1.0.4.1:*:*:*:*:*:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + const includedRangeResponse = controller.applySearchPostFilters( + advancedResponse(1, [rangeCandidate]), + { + cpeName: 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:6.1:*:*:*:*:*:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + const excludedRangeResponse = controller.applySearchPostFilters( + advancedResponse(1, [rangeCandidate]), + { + cpeName: 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:6.2:*:*:*:*:*:*:*', + isVulnerable: true + }, + { + from: 0, + resultsPerPage: 20 + } + ); + + expect(exactResponse.data.hits.total.value).toBe(0); + expect(includedRangeResponse.data.hits.hits).toStrictEqual([rangeCandidate]); + expect(excludedRangeResponse.data.hits.total.value).toBe(0); + }); + test('applySearchPostFilters preserves NVD-compatible CPE NA matches from source record ranges', () => { const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; const candidates = [ diff --git a/routes/swagger.js b/routes/swagger.js index 4fa72d5..c6dacb3 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, @@ -193,7 +193,7 @@ const searchRequestProperties = { } ), versionStart: searchRequestProperty( - 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', + 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionStart, @@ -208,7 +208,7 @@ const searchRequestProperties = { } ), versionEnd: searchRequestProperty( - 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', + 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionEnd, diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 8fcee19..63cd038 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -156,9 +156,12 @@ describe('CPE helpers', () => { expect(cpe.buildCompatibleCriteriaPrefixes(parsed, 3)[0]).toBe('cpe:2.3:a:vendor:product\\:edition:'); }); - test('compareCpeVersions compares numeric version segments semantically', () => { + test('compareCpeVersions compares numeric version segments semantically for range ordering', () => { expect(cpe.compareCpeVersions('2.10', '2.9')).toBeGreaterThan(0); expect(cpe.compareCpeVersions('10.05.1', '10.5.1')).toBe(0); + expect(cpe.compareCpeVersions('10.5.0', '10.05.1')).toBeLessThan(0); + expect(cpe.compareCpeVersions('6.1', '6.02')).toBeLessThan(0); + expect(cpe.compareCpeVersions('6.2', '6.02')).toBe(0); expect(cpe.compareCpeVersions('1', '1.0')).toBe(0); expect(cpe.compareCpeVersions('1.0.0', '1')).toBe(0); expect(cpe.compareCpeVersions('1', '1.0.1')).toBeLessThan(0); @@ -222,6 +225,43 @@ describe('CPE helpers', () => { ).toBe(false); }); + test('cpeMatchAppliesToCpeName ignores leading zeros only when evaluating range boundaries', () => { + const ghostscriptRange = { + criteria: 'cpe:2.3:a:artifex:ghostscript:*:*:*:*:*:*:*:*', + versionEndExcluding: '10.05.1' + }; + const buffaloRange = { + criteria: 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:*:*:*:*:*:*:*:*', + versionEndExcluding: '6.02' + }; + + expect( + cpe.cpeMatchAppliesToCpeName(ghostscriptRange, 'cpe:2.3:a:artifex:ghostscript:10.5.0:*:*:*:*:*:*:*') + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName(ghostscriptRange, 'cpe:2.3:a:artifex:ghostscript:10.5.1:*:*:*:*:*:*:*') + ).toBe(false); + expect( + cpe.cpeMatchAppliesToCpeName(buffaloRange, 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:6.1:*:*:*:*:*:*:*') + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName(buffaloRange, 'cpe:2.3:o:buffalo:wsr3600be4-kh_firmware:6.2:*:*:*:*:*:*:*') + ).toBe(false); + }); + + test('cpeMatchAppliesToCpeName preserves leading zeros for exact criteria versions', () => { + const exactCriteria = { + criteria: 'cpe:2.3:o:linksys:re6250_firmware:1.0.04.001:*:*:*:*:*:*:*' + }; + + expect( + cpe.cpeMatchAppliesToCpeName(exactCriteria, 'cpe:2.3:o:linksys:re6250_firmware:1.0.04.001:*:*:*:*:*:*:*') + ).toBe(true); + expect( + cpe.cpeMatchAppliesToCpeName(exactCriteria, 'cpe:2.3:o:linksys:re6250_firmware:1.0.4.1:*:*:*:*:*:*:*') + ).toBe(false); + }); + test('cpeMatchAppliesToCpeName treats prerelease qualifiers as earlier than the unqualified release', () => { const criteria = { criteria: 'cpe:2.3:a:vendor:product:*:*:*:*:*:*:*:*', From 0804d39920e47ddc5c810ab194e1506a14ec1808 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 29 Jul 2026 08:20:50 -0400 Subject: [PATCH 18/20] docs: remove NVD alignment language --- README.md | 10 +++++----- api-docs/openapi.json | 6 +++--- api-docs/openapi.test.js | 6 +++--- controllers/middleware.js | 2 +- routes/swagger.js | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index f7605a6..9dfcb80 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ String inputs are bounded before query construction: | `versionStart`, `versionEnd` | 256 characters each | | CVSS metric fields | 256 characters, 32 slash-delimited tokens, and 32 characters per token | -Requests that exceed a limit return HTTP 400 before contacting OpenSearch. These bounds limit input size and query complexity without changing NVD-style CPE matching semantics. +Requests that exceed a limit return HTTP 400 before contacting OpenSearch. These bounds limit input size and query complexity without changing the documented CPE matching semantics. Use `cveIds` to request up to 100 CVE records by comma-separated CVE ID. @@ -152,13 +152,13 @@ curl -X POST "http://localhost:3000/search" \ The valid query can match stored CPE Match Criteria that use either a concrete version or a wildcard version plus range fields such as `versionStartIncluding`, `versionStartExcluding`, `versionEndIncluding`, or `versionEndExcluding`. In accordance with the CPE 2.3 Name Matching specification, CPE string literal comparisons are insensitive to lexical case, so an input component such as `macos` matches stored criteria containing `macOS`. Every concrete non-version component in the input, such as edition, language, target software, or target hardware, must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks. Optional input components set to `*` remain broad matches. Use `virtualMatchString` instead of `cpeName` when searching for partial CPE Match String values such as all versions of a product. -The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. To match NVD `cpeName` behavior, numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This compatibility rule can produce different range behavior than a literal reading of source CVE Record applicability fields because NVD applies its own CPE enrichment when building CPE Match Criteria. +The version component `-` is the CPE logical value `NA`, meaning that no meaningful version applies. Numeric version range boundaries on wildcard-version criteria do not exclude an asset whose version is `-`; an exact concrete criteria version still does not match `-`, and an exclusive `-` boundary still excludes it. This rule can produce different range behavior than a literal reading of source CVE Record applicability fields. Set `isVulnerable` to `true` with `cpeName` to require the matching CPE Match Criteria entry itself to be marked vulnerable. A vulnerable flag on an unrelated CPE entry does not satisfy the request. Omitting `isVulnerable`, or setting it to `false`, does not apply a vulnerability restriction. ### Virtual Match String Search -Use `virtualMatchString` when a user needs NVD-style CPE Match Criteria compatibility matching instead of concrete asset matching. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches, matching NVD's broad CPE Match String behavior. +Use `virtualMatchString` when a user needs CPE Match Criteria compatibility matching instead of concrete asset matching. String literal components are compared without regard to lexical case. Omitted trailing components and `*` components act as broad matches. Partial product example: @@ -190,9 +190,9 @@ Version ranges use overlap semantics. A stored wildcard-version CPE Match Criter CPE version comparisons used by both `cpeName` applicability and `virtualMatchString` version ranges use Maven-style qualifier precedence: `alpha`/`a` < `beta`/`b` < `milestone`/`m` < `rc`/`cr` < `snapshot` < an unqualified release/`ga`/`final`/`release` < `sp` < unknown qualifiers. Unknown qualifiers, including `hf`, sort after known qualifiers in case-insensitive lexical order. This means a range beginning at `1.0.0-rc1` includes `1.0.0`, while a range ending before `1.0.0` can include prerelease versions. -As observed in the NVD CVE API on July 28, 2026, numeric segments used in version range comparisons ignore leading zeros. A stored exclusive upper bound of `10.05.1` therefore includes `10.5.0`, and CVE-2026-33280's exclusive `6.02` upper bound includes `6.1` but excludes `6.2`. This normalization applies only to range comparison. Exact CPE Match Criteria preserve leading zeros, so the CVE-2025-9392 versions `1.0.04.001` and `1.0.4.1` are not equivalent. This NVD compatibility behavior is not a universal firmware-versioning rule and can differ from a vendor's actual release sequence. +Numeric segments used in version range comparisons ignore leading zeros. A stored exclusive upper bound of `10.05.1` therefore includes `10.5.0`, and CVE-2026-33280's exclusive `6.02` upper bound includes `6.1` but excludes `6.2`. This normalization applies only to range comparison. Exact CPE Match Criteria preserve leading zeros, so the CVE-2025-9392 versions `1.0.04.001` and `1.0.4.1` are not equivalent. This comparison rule is not a universal firmware-versioning rule and can differ from a vendor's actual release sequence. -Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. That would intentionally diverge from NVD behavior and should be handled as a policy decision before implementation. +Future recommendation: broad `virtualMatchString` values can be expensive because they may require large candidate scans plus semantic CPE post-filtering. Requiring concrete leading components, such as `part` and `vendor` for normal searches and `part`, `vendor`, and `product` for version range searches, would reduce the most expensive public search shapes. Narrowing the accepted searches should be handled as a policy decision before implementation. `POST /webSearch` is deprecated in favor of `POST /search`. It remains enabled by default during consumer migration and can be disabled without a code change by setting `WebSearchEnabled=false` and restarting the service. When disabled, it returns HTTP `410` with the stable error code `WEB_SEARCH_DISABLED` before validating the request or contacting OpenSearch. diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 00cc2d7..1c41dd0 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -419,7 +419,7 @@ "type": "string", "maxLength": 2048, "example": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", - "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." + "description": "Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches." }, "isVulnerable": { "type": "boolean", @@ -469,7 +469,7 @@ "type": "string", "maxLength": 256, "example": "2.2", - "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." + "description": "Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionStartType": { "type": "string", @@ -483,7 +483,7 @@ "type": "string", "maxLength": 256, "example": "2.6", - "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." + "description": "Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component." }, "versionEndType": { "type": "string", diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index d5216d1..7d0ba92 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -79,7 +79,7 @@ describe('OpenAPI contract', () => { expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches'); expect(schema.properties.cpeName.description).toContain('insensitive to lexical case'); expect(schema.properties.cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); - expect(schema.properties.cpeName.description).toContain('July 28, 2026'); + expect(schema.properties.cpeName.description).not.toContain('NVD'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); @@ -87,7 +87,7 @@ describe('OpenAPI contract', () => { expect(schema.properties.versionStart.description).toContain('versionStartType'); expect(schema.properties.versionStart.description).toContain('less than versionEnd'); expect(schema.properties.versionStart.description).toContain('ranges ignore leading zeros'); - expect(schema.properties.versionStart.description).toContain('July 28, 2026'); + expect(schema.properties.versionStart.description).not.toContain('NVD'); expect(schema.properties.versionStart.description).toContain('Maven-style ordering'); expect(schema.properties.versionEnd.description).toContain('versionEndType'); expect(schema.properties.versionEnd.description).toContain('exact CPE Match Criteria preserve leading zeros'); @@ -133,7 +133,7 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('version range fields'); expect(cpeName.description).toContain('insensitive to lexical case'); expect(cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); - expect(cpeName.description).toContain('July 28, 2026'); + expect(cpeName.description).not.toContain('NVD'); expect(cpeName.description).toContain('logical value NA'); expect(cpeName.description).toContain('numeric range boundaries'); expect(cpeName.description).toContain('HTTP 404'); diff --git a/controllers/middleware.js b/controllers/middleware.js index 351e7af..8eedf02 100644 --- a/controllers/middleware.js +++ b/controllers/middleware.js @@ -258,7 +258,7 @@ function validateParams(req, res, next) { return res.status(400).json(error.missingCpeNameError()); } - //Ensures cpeName follows NVD API requirements for required CPE components + //Ensures cpeName contains the required concrete CPE components if (params?.cpeName && !cpe.isValidCpeName(params.cpeName)) { return res.status(404).json(error.invalidCpeNameError()); } diff --git a/routes/swagger.js b/routes/swagger.js index c6dacb3..ecfe77e 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -127,7 +127,7 @@ const searchRequestProperties = { example: 'security@example.org' }), cpeName: searchRequestProperty( - 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; for NVD compatibility, numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', + 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.cpeName, @@ -193,7 +193,7 @@ const searchRequestProperties = { } ), versionStart: searchRequestProperty( - 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', + 'Sets the starting version bound for a CPE Match String range search. versionStartType and virtualMatchString are required when versionStart is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionStart, @@ -208,7 +208,7 @@ const searchRequestProperties = { } ), versionEnd: searchRequestProperty( - 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros for NVD compatibility, based on behavior observed in the NVD CVE API on July 28, 2026; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', + 'Sets the ending version bound for a CPE Match String range search. versionEndType and virtualMatchString are required when versionEnd is provided. When both bounds are provided, versionStart must be less than versionEnd; equal values require both bounds to be including. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. virtualMatchString must not include a version component.', { type: 'string', maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.versionEnd, From 370b8c77a8853147062ec81b0d1a5829af7f5924 Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Wed, 29 Jul 2026 08:38:16 -0400 Subject: [PATCH 19/20] test: use direct CPE behavior descriptions --- api-docs/openapi.test.js | 3 --- controllers/middleware.test.js | 2 +- controllers/searchController.test.js | 2 +- utils/cpe.test.js | 6 +++--- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 7d0ba92..060097e 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -79,7 +79,6 @@ describe('OpenAPI contract', () => { expect(schema.properties.cpeName.description).toContain('optional * components remain broad matches'); expect(schema.properties.cpeName.description).toContain('insensitive to lexical case'); expect(schema.properties.cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); - expect(schema.properties.cpeName.description).not.toContain('NVD'); expect(schema.properties.versionStartType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); @@ -87,7 +86,6 @@ describe('OpenAPI contract', () => { expect(schema.properties.versionStart.description).toContain('versionStartType'); expect(schema.properties.versionStart.description).toContain('less than versionEnd'); expect(schema.properties.versionStart.description).toContain('ranges ignore leading zeros'); - expect(schema.properties.versionStart.description).not.toContain('NVD'); expect(schema.properties.versionStart.description).toContain('Maven-style ordering'); expect(schema.properties.versionEnd.description).toContain('versionEndType'); expect(schema.properties.versionEnd.description).toContain('exact CPE Match Criteria preserve leading zeros'); @@ -133,7 +131,6 @@ describe('OpenAPI contract', () => { expect(cpeName.description).toContain('version range fields'); expect(cpeName.description).toContain('insensitive to lexical case'); expect(cpeName.description).toContain('exact CPE Match Criteria preserve leading zeros'); - expect(cpeName.description).not.toContain('NVD'); expect(cpeName.description).toContain('logical value NA'); expect(cpeName.description).toContain('numeric range boundaries'); expect(cpeName.description).toContain('HTTP 404'); diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index d86a6ce..60ddf3c 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -149,7 +149,7 @@ describe('middleware validation function', () => { ); }); - test('ValidateParams calls next() for broad NVD-style virtualMatchString values', () => { + test('ValidateParams accepts broad virtualMatchString values', () => { validate({ virtualMatchString: 'cpe:2.3:o:*:windows_10_1809' }); diff --git a/controllers/searchController.test.js b/controllers/searchController.test.js index 4c5bebd..2a116f5 100644 --- a/controllers/searchController.test.js +++ b/controllers/searchController.test.js @@ -674,7 +674,7 @@ describe('Search controller SEARCH handler', () => { expect(excludedRangeResponse.data.hits.total.value).toBe(0); }); - test('applySearchPostFilters preserves NVD-compatible CPE NA matches from source record ranges', () => { + test('applySearchPostFilters preserves CPE NA matches across numeric version ranges', () => { const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; const candidates = [ cpeHit('CVE-2020-17153', { diff --git a/utils/cpe.test.js b/utils/cpe.test.js index 63cd038..916a0d9 100644 --- a/utils/cpe.test.js +++ b/utils/cpe.test.js @@ -112,7 +112,7 @@ describe('CPE helpers', () => { ); }); - test('buildVirtualMatchStringCriteriaRegexp builds an NVD-style partial criteria pattern', () => { + test('buildVirtualMatchStringCriteriaRegexp builds a partial criteria pattern', () => { const parsed = cpe.parseVirtualMatchString('cpe:2.3:o:microsoft:windows_10_1809'); expect(cpe.buildVirtualMatchStringCriteriaRegexp(parsed)).toBe( @@ -351,7 +351,7 @@ describe('CPE helpers', () => { ).toBe(true); }); - test('cpeMatchAppliesToCpeName treats the CPE NA version as nonnumeric for NVD-compatible ranges', () => { + test('cpeMatchAppliesToCpeName treats the CPE NA version as nonnumeric for range boundaries', () => { const cpeName = 'cpe:2.3:a:microsoft:edge:-:*:*:*:*:android:*:*'; const criteria = 'cpe:2.3:a:microsoft:edge:*:*:*:*:*:android:*:*'; @@ -576,7 +576,7 @@ describe('CPE helpers', () => { ).toBe(false); }); - test('cpeMatchMatchesVirtualMatchString matches full and partial NVD-style match strings', () => { + test('cpeMatchMatchesVirtualMatchString matches full and partial match strings', () => { const windowsX86 = { criteria: 'cpe:2.3:o:microsoft:windows_10_1809:-:*:*:*:*:*:x86:*' }; From 5c7d0ed07eb03d7940bd09a4e210140259818f1f Mon Sep 17 00:00:00 2001 From: Andrew Foote Date: Thu, 30 Jul 2026 07:29:48 -0400 Subject: [PATCH 20/20] feat: remove sourceIdentifier filter for 0.4.0-beta --- README.md | 1 - api-docs/openapi.json | 11 ++-------- api-docs/openapi.test.js | 6 ++++++ controllers/middleware.test.js | 13 ++++++------ package-lock.json | 4 ++-- package.json | 2 +- routes/index.js | 1 - routes/swagger.js | 8 ++----- utils/constants.js | 4 ---- utils/searchQueryBuilder.test.js | 36 ++++---------------------------- 10 files changed, 24 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 9dfcb80..6201913 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,6 @@ String inputs are bounded before query construction: | `keywordSearch` | 512 characters and 64 whitespace-delimited terms | | `cveIds` | 4,096 characters overall, 100 IDs, and 64 characters per ID | | `cweId` | 32 characters | -| `sourceIdentifier` | 512 characters | | `cpeName` | 2,048 characters | | `virtualMatchString` | 2,048 characters | | `versionStart`, `versionEnd` | 256 characters each | diff --git a/api-docs/openapi.json b/api-docs/openapi.json index 1c41dd0..61279e4 100644 --- a/api-docs/openapi.json +++ b/api-docs/openapi.json @@ -1,7 +1,7 @@ { "openapi": "3.0.2", "info": { - "version": "0.3.0-beta", + "version": "0.4.0-beta", "title": "CVE Search API", "description": "CVE Search API to retrieve all matching CVE records after applying request inputs as filters.", "contact": { @@ -108,7 +108,6 @@ "cvssV3_0Metrics": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "cvssV3_1Metrics": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "cvssV4Metrics": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", - "sourceIdentifier": "security@example.org", "cpeName": "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", "isVulnerable": false, "lastModStartDate": "2024-01-01T00:00:00.000Z", @@ -409,12 +408,6 @@ "example": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", "description": "Returns CVE records that contain the provided CVSS v4.0 vector string. Values are limited to 256 characters, 32 slash-delimited metric tokens, and 32 characters per metric token." }, - "sourceIdentifier": { - "type": "string", - "maxLength": 512, - "example": "security@example.org", - "description": "Returns CVE records that contain the provided source identifier." - }, "cpeName": { "type": "string", "maxLength": 2048, @@ -503,4 +496,4 @@ } } } -} +} \ No newline at end of file diff --git a/api-docs/openapi.test.js b/api-docs/openapi.test.js index 060097e..0b49ee4 100644 --- a/api-docs/openapi.test.js +++ b/api-docs/openapi.test.js @@ -2,6 +2,7 @@ const fs = require('fs'); const os = require('os'); const path = require('path'); const openapi = require('./openapi.json'); +const packageInfo = require('../package.json'); const constants = require('../utils/constants'); const { generateOpenApi } = require('../routes/swagger'); @@ -33,6 +34,10 @@ describe('OpenAPI contract', () => { ]); }); + test('uses the project package version', () => { + expect(openapi.info.version).toBe(packageInfo.version); + }); + test('all path parameter references resolve to component parameters', () => { Object.values(openapi.paths).forEach((pathItem) => { Object.values(pathItem).forEach((operation) => { @@ -83,6 +88,7 @@ describe('OpenAPI contract', () => { expect(schema.properties.versionEndType.enum).toStrictEqual(constants.CPE_VERSION_RANGE_TYPES); expect(schema.properties.resultsPerPage.maximum).toBe(constants.MAX_RESULTS_LIMIT); expect(schema.properties.cveId).toBeUndefined(); + expect(schema.properties.sourceIdentifier).toBeUndefined(); expect(schema.properties.versionStart.description).toContain('versionStartType'); expect(schema.properties.versionStart.description).toContain('less than versionEnd'); expect(schema.properties.versionStart.description).toContain('ranges ignore leading zeros'); diff --git a/controllers/middleware.test.js b/controllers/middleware.test.js index 60ddf3c..5079be4 100644 --- a/controllers/middleware.test.js +++ b/controllers/middleware.test.js @@ -397,7 +397,6 @@ describe('middleware validation function', () => { ['cvssV3_0Metrics', 'CVSS:3.0/AV:N/AC:L'], ['cvssV3_1Metrics', 'CVSS:3.1/PR:N/UI:R'], ['cvssV4Metrics', 'CVSS:4.0/AV:N/AC:L'], - ['sourceIdentifier', 'CISA'], ['cpeName', 'cpe:2.3:a:vendor:product:1.0:*:*:*:*:*:*:*'], ['isVulnerable', 'false'], ['lastModStartDate', '2024-01-01T00:00:00.000Z'], @@ -415,12 +414,15 @@ describe('middleware validation function', () => { expectValidationSuccess(); }); - test('ValidateParams returns an error when deprecated cveId is provided.', () => { + test.each([ + ['cveId', 'CVE-2024-0001'], + ['sourceIdentifier', 'CISA'] + ])('ValidateParams returns an error when unsupported %s is provided.', (param, value) => { validate({ - cveId: 'CVE-2024-0001' + [param]: value }); - expectValidationError('INVALID_PARAM', 'cveId is not supported by the Search API.'); + expectValidationError('INVALID_PARAM', `${param} is not supported by the Search API.`); }); test.each([ @@ -461,8 +463,7 @@ describe('middleware validation function', () => { test.each([ ['keywordSearch', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.keywordSearch)], - ['cweId', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.cweId)], - ['sourceIdentifier', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.sourceIdentifier)] + ['cweId', 'a'.repeat(constants.SEARCH_STRING_LENGTH_LIMITS.cweId)] ])('ValidateParams accepts %s at its maximum length.', (param, value) => { validate({ [param]: value diff --git a/package-lock.json b/package-lock.json index bae0a62..61f936e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cve-search-api", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cve-search-api", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "dependencies": { "cors": "^2.8.5", "cve-core": "github:CVEProject/cve-core.git#dev-package-test", diff --git a/package.json b/package.json index de45e6f..2a59a9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cve-search-api", - "version": "0.3.0-beta", + "version": "0.4.0-beta", "scripts": { "dev": "node routes/swagger.js && nodemon server.js", "coverage": "jest --coverage", diff --git a/routes/index.js b/routes/index.js index 7cc0568..adca945 100644 --- a/routes/index.js +++ b/routes/index.js @@ -190,7 +190,6 @@ router.post( cvssV3_0Metrics: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvssV3_1Metrics: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", cvssV4Metrics: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", - sourceIdentifier: "security@example.org", cpeName: "cpe:2.3:a:freetype:freetype:2.8.1:*:*:*:*:*:*:*", isVulnerable: false, lastModStartDate: "2024-01-01T00:00:00.000Z", diff --git a/routes/swagger.js b/routes/swagger.js index ecfe77e..1cf65db 100644 --- a/routes/swagger.js +++ b/routes/swagger.js @@ -1,5 +1,6 @@ const path = require('path'); const swaggerAutogen = require('swagger-autogen'); +const packageInfo = require('../package.json'); const constants = require('../utils/constants'); const projectRoot = path.join(__dirname, '..'); @@ -121,11 +122,6 @@ const searchRequestProperties = { example: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N' } ), - sourceIdentifier: searchRequestProperty('Returns CVE records that contain the provided source identifier.', { - type: 'string', - maxLength: constants.SEARCH_STRING_LENGTH_LIMITS.sourceIdentifier, - example: 'security@example.org' - }), cpeName: searchRequestProperty( 'Returns CVE records whose CPE Match Criteria apply to the provided CPE 2.3 asset name. CPE string literal comparisons are insensitive to lexical case. The part, vendor, product, and version components must contain values other than *. Every concrete non-version component must be compatible with the same CPE Match Criteria entry that satisfies the version and vulnerability checks; optional * components remain broad matches. The concrete version can match stored criteria with the same version or criteria with a wildcard version and version range fields. Numeric version segments in ranges ignore leading zeros; exact CPE Match Criteria preserve leading zeros. Version qualifiers use Maven-style ordering, with prerelease qualifiers before an unqualified release and sp or unknown qualifiers after it. The version - represents the CPE logical value NA; numeric range boundaries on wildcard-version criteria do not exclude it. Invalid cpeName values return HTTP 404. Use virtualMatchString for partial CPE Match String searches.', { @@ -235,7 +231,7 @@ const searchRequestProperties = { const doc = { openapi: '3.0.2', info: { - version: '0.3.0-beta', + version: packageInfo.version, title: 'CVE Search API', description: 'CVE Search API to retrieve all matching CVE records after applying request inputs as filters.', contact: { diff --git a/utils/constants.js b/utils/constants.js index e3f824e..85ae9f3 100644 --- a/utils/constants.js +++ b/utils/constants.js @@ -11,7 +11,6 @@ const SEARCH_STRING_LENGTH_LIMITS = Object.freeze({ keywordSearch: 512, cveIds: 4096, cweId: 32, - sourceIdentifier: 512, cpeName: 2048, versionStart: 256, versionEnd: 256, @@ -56,7 +55,6 @@ const cpeMatchFields = { * cvssV3_0Metrics: CVSSv3_0 vector string * cvssV3_1Metrics: CVSSv3_1 vector string * cvssV4Metrics: CVSSv4 vector string - * sourceIdentifier: string * cpeName: string, name of CPE to search for * isVulnerable: boolean * versionStart: string, lower version bound used with virtualMatchString @@ -79,7 +77,6 @@ const fields = { cvssV3Metrics: ['containers.cna.metrics.cvssV3_0.vectorString', 'containers.cna.metrics.cvssV3_1.vectorString'], cvssV4Severity: 'containers.cna.metrics.cvssV4_0.baseSeverity', cvssV4Metrics: 'containers.cna.metrics.cvssV4_0.vectorString', - sourceIdentifier: 'containers.cna.source.discovery', cpeName: cpeMatchFields.criteria, isVulnerable: cpeMatchFields.vulnerable, virtualMatchString: cpeMatchFields.criteria @@ -115,7 +112,6 @@ const searchQueryParams = [ 'cvssV3_0Metrics', 'cvssV3_1Metrics', 'cvssV4Metrics', - 'sourceIdentifier', 'cpeName', 'isVulnerable', 'lastModStartDate', diff --git a/utils/searchQueryBuilder.test.js b/utils/searchQueryBuilder.test.js index 9430664..b26f879 100644 --- a/utils/searchQueryBuilder.test.js +++ b/utils/searchQueryBuilder.test.js @@ -306,34 +306,6 @@ describe('searchQueryBuilder buildQueryStrings() function', () => { expect(output).toStrictEqual(testQueryString); }); - test('buildQueryStrings() function correctly builds mixed query objects when multiple query string parameters are passed.', () => { - const testParams = { - keywordSearch: 'test* params*', - sourceIdentifier: 'EXTERNAL' - }; - - const testQueryString = [ - { - simple_query_string: { - query: 'test* params*', - default_operator: 'AND', - fields: ['containers.cna.descriptions.value'] - } - }, - { - query_string: { - query: 'EXTERNAL', - default_operator: 'AND', - fields: ['containers.cna.source.discovery'] - } - } - ]; - - const output = queryBuilder.buildQueryStrings(testParams); - expect(output).toHaveLength(2); - expect(output).toStrictEqual(testQueryString); - }); - test('buildQueryStrings() ignores virtualMatchString because it is handled as a structured filter.', () => { const output = queryBuilder.buildQueryStrings({ virtualMatchString: 'cpe:2.3:o:microsoft:windows_10_1809' @@ -346,13 +318,13 @@ describe('searchQueryBuilder buildQueryStrings() function', () => { describe('searchQueryBuilder getFilterFields() function', () => { test('getFilterFields() function correctly builds array of one matchPhrase object when one parameter is passed.', () => { const testParams = { - sourceIdentifier: 'EXTERNAL' + cweId: 'CWE-79' }; const testMatchPhrase = [ { match_phrase: { - 'containers.cna.source.discovery': 'EXTERNAL' + 'containers.cna.problemTypes.descriptions.cweId': 'CWE-79' } } ]; @@ -364,14 +336,14 @@ describe('searchQueryBuilder getFilterFields() function', () => { test('getFilterFields() function correctly builds default and severity filter objects when multiple parameters are passed.', () => { const testParams = { - sourceIdentifier: 'EXTERNAL', + cweId: 'CWE-79', cvssV4Severity: 'HIGH' }; const testMatchPhrase = [ { match_phrase: { - 'containers.cna.source.discovery': 'EXTERNAL' + 'containers.cna.problemTypes.descriptions.cweId': 'CWE-79' } }, {