diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f6317a2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Normalize text files and keep repository line endings as LF +* text=auto eol=lf + +# Keep Windows-specific scripts as CRLF +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf diff --git a/.github/workflows/dotnet-tests.yml b/.github/workflows/test-csharp.yml similarity index 91% rename from .github/workflows/dotnet-tests.yml rename to .github/workflows/test-csharp.yml index 4d377db..01fe5f8 100644 --- a/.github/workflows/dotnet-tests.yml +++ b/.github/workflows/test-csharp.yml @@ -5,8 +5,11 @@ name: Run .NET Tests on: pull_request: - push: - branches: [main] + branches: + - main + paths: + - "csharp/**" + - ".github/workflows/test-csharp.yml" workflow_dispatch: jobs: diff --git a/.github/workflows/test-nodejs.yml b/.github/workflows/test-nodejs.yml new file mode 100644 index 0000000..b811caf --- /dev/null +++ b/.github/workflows/test-nodejs.yml @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: Run NodeJS Tests + +on: + pull_request: + branches: + - main + paths: + - "nodejs/**" + - ".github/workflows/test-nodejs.yml" + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + + strategy: + matrix: + node-version: [20.x, 22.x, 24.x] + + steps: + - uses: actions/checkout@v5 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "npm" + cache-dependency-path: "nodejs/package-lock.json" + + - name: Install dependencies + working-directory: nodejs + run: npm ci + + - name: Run unit tests + working-directory: nodejs + run: npm run test:unit + + - name: Run functional tests + working-directory: nodejs + run: npm run test:functional diff --git a/.github/workflows/update-ip-ranges.yml b/.github/workflows/update-ip-ranges.yml index a64766a..6fa58b1 100644 --- a/.github/workflows/update-ip-ranges.yml +++ b/.github/workflows/update-ip-ranges.yml @@ -7,13 +7,13 @@ on: pull_request: paths: - 'config/IPAddressRanges.json' - - 'nodejs/config/IPAddressRanges.ts' + - 'nodejs/src/IPAddressRanges.ts' - 'csharp/src/IPAddressRanges.cs' push: branches: [main] paths: - 'config/IPAddressRanges.json' - - 'nodejs/config/IPAddressRanges.ts' + - 'nodejs/src/IPAddressRanges.ts' - 'csharp/src/IPAddressRanges.cs' workflow_dispatch: # Allow manual triggering @@ -41,10 +41,10 @@ jobs: - name: Verify generated files are up-to-date run: | - if ! git diff --exit-code nodejs/config/IPAddressRanges.ts csharp/src/IPAddressRanges.cs; then + if ! git diff --exit-code nodejs/src/IPAddressRanges.ts csharp/src/IPAddressRanges.cs; then echo "❌ Generated IP address range files are out of sync!" echo "The following files need to be regenerated:" - git diff --name-only nodejs/config/IPAddressRanges.ts csharp/src/IPAddressRanges.cs + git diff --name-only nodejs/src/IPAddressRanges.ts csharp/src/IPAddressRanges.cs echo "" echo "Please run the following commands locally and commit the results:" echo " ./scripts/build-ip-ranges-nodejs.sh" diff --git a/csharp/src/AntiSSRFPolicy.cs b/csharp/src/AntiSSRFPolicy.cs index 5e765de..ecf4a0c 100644 --- a/csharp/src/AntiSSRFPolicy.cs +++ b/csharp/src/AntiSSRFPolicy.cs @@ -146,7 +146,7 @@ public AntiSSRFPolicy(PolicyConfigOptions config) AddXFFHeader = false; break; default: - throw new ArgumentOutOfRangeException(nameof(config), config, "Invalid policy option"); + throw new ArgumentOutOfRangeException(nameof(config), config, "Argument must be a valid PolicyConfigOptions value"); } } @@ -224,10 +224,10 @@ public void AddDeniedHeaders(string[]? deniedHeaders) foreach (string headerName in deniedHeaders) { if (headerName is null) - throw new ArgumentNullException(nameof(deniedHeaders), "Header name cannot be null"); + throw new ArgumentNullException(nameof(deniedHeaders), "Headers cannot be null"); if (string.IsNullOrWhiteSpace(headerName)) - throw new ArgumentException($"Header name cannot be empty or whitespace", nameof(deniedHeaders)); + throw new ArgumentException($"Headers cannot be empty or whitespace", nameof(deniedHeaders)); } _deniedHeaders.AddRange(deniedHeaders); @@ -252,10 +252,10 @@ public void AddRequiredHeaders(string[]? requiredHeaders) foreach (string headerName in requiredHeaders) { if (headerName is null) - throw new ArgumentNullException(nameof(requiredHeaders), "Header name cannot be null"); + throw new ArgumentNullException(nameof(requiredHeaders), "Headers cannot be null"); if (string.IsNullOrWhiteSpace(headerName)) - throw new ArgumentException($"Header name cannot be empty or whitespace", nameof(requiredHeaders)); + throw new ArgumentException($"Headers cannot be empty or whitespace", nameof(requiredHeaders)); } _requiredHeaders.AddRange(requiredHeaders); diff --git a/nodejs/.prettierignore b/nodejs/.prettierignore new file mode 100644 index 0000000..a091145 --- /dev/null +++ b/nodejs/.prettierignore @@ -0,0 +1,11 @@ +node_modules/** +out/** +*.md +.gitignore +.npmrc +.config +*.json +**/*.json +**/*.tgz +temp-lib +src/IPAddressRanges.ts diff --git a/nodejs/.prettierrc b/nodejs/.prettierrc new file mode 100644 index 0000000..d522883 --- /dev/null +++ b/nodejs/.prettierrc @@ -0,0 +1,9 @@ +{ + "arrowParens": "always", + "bracketSpacing": true, + "endOfLine": "lf", + "printWidth": 120, + "singleQuote": false, + "tabWidth": 4, + "trailingComma": "none" +} diff --git a/nodejs/README.md b/nodejs/README.md new file mode 100644 index 0000000..30404ce --- /dev/null +++ b/nodejs/README.md @@ -0,0 +1 @@ +TODO \ No newline at end of file diff --git a/nodejs/eslint.config.mjs b/nodejs/eslint.config.mjs new file mode 100644 index 0000000..29a37c1 --- /dev/null +++ b/nodejs/eslint.config.mjs @@ -0,0 +1,38 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import security from "eslint-plugin-security"; + +export default tseslint.config({ + files: ["**/*.ts"], + extends: [eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, security.configs.recommended], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname + } + }, + plugins: { + eslint: eslint + }, + rules: { + "func-style": ["error", "declaration"], + "@typescript-eslint/naming-convention": [ + "error", + { + selector: ["class"], + format: ["PascalCase"] + }, + { + selector: ["variable"], + modifiers: ["const", "exported"], + format: ["UPPER_CASE"] + }, + { + selector: "memberLike", + modifiers: ["private"], + format: ["camelCase"], + leadingUnderscore: "require" + } + ] + } +}); diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json new file mode 100644 index 0000000..f335b8d --- /dev/null +++ b/nodejs/package-lock.json @@ -0,0 +1,4683 @@ +{ + "name": "@azuresecurity/antissrf", + "version": "1.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@azuresecurity/antissrf", + "version": "1.2.0", + "dependencies": { + "@types/mocha": "^10.0.10", + "mocha": "^11.7.5" + }, + "devDependencies": { + "@eslint/js": "^9.20.0", + "@types/follow-redirects": "^1.14.4", + "@types/node-fetch": "^2.6.13", + "axios": "^1.12.2", + "eslint": "^9.20.0", + "eslint-plugin-security": "^3.0.1", + "follow-redirects": "^1.15.9", + "node-fetch": "^3.3.2", + "nyc": "^17.1.0", + "prettier": "^3.5.3", + "tar": "^7.4.3", + "ts-node": "^10.9.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@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/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.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "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.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.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/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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/load-nyc-config/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": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/follow-redirects": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@types/follow-redirects/-/follow-redirects-1.14.4.tgz", + "integrity": "sha512-GWXfsD0Jc1RWiFmMuMFCpXMzi9L7oPDVwxUnZdg89kDNnqsRfUKXEtUYtA98A6lig1WXH/CYY/fvPW9HuN5fTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", + "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/type-utils": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", + "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", + "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.0", + "@typescript-eslint/types": "^8.59.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", + "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", + "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", + "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", + "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", + "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.0", + "@typescript-eslint/tsconfig-utils": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/visitor-keys": "8.59.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", + "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.0", + "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", + "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "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/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/append-transform": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz", + "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-require-extensions": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "dev": true, + "license": "MIT" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", + "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz", + "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "license": "ISC" + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caching-transform": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", + "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001788", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", + "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/default-require-extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", + "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + }, + "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", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "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==", + "dev": true, + "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/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.342", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.342.tgz", + "integrity": "sha512-GTuy59SdGxYgz+HN8KwOjFAVF2gfoKEmv0PFholcvVtbI9GPDND0m6ynGX3gAKOavcHRLrcfNy0QMbHbAemYdw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-security": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz", + "integrity": "sha512-XjVGBhtDZJfyuhIxnQ/WMm385RbX3DBu7H1J7HNNhmB2tnGxMeqVSnYv79oAj992ayvIBZghsymwkYFS6cGH4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "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-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fromentries": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", + "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "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", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasha": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", + "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz", + "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-processinfo": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", + "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", + "dev": true, + "license": "ISC", + "dependencies": { + "archy": "^1.0.0", + "cross-spawn": "^7.0.3", + "istanbul-lib-coverage": "^3.2.0", + "p-map": "^3.0.0", + "rimraf": "^3.0.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "process-on-spawn": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nyc": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz", + "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "caching-transform": "^4.0.0", + "convert-source-map": "^1.7.0", + "decamelize": "^1.2.0", + "find-cache-dir": "^3.2.0", + "find-up": "^4.1.0", + "foreground-child": "^3.3.0", + "get-package-type": "^0.1.0", + "glob": "^7.1.6", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-hook": "^3.0.0", + "istanbul-lib-instrument": "^6.0.2", + "istanbul-lib-processinfo": "^2.0.2", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "make-dir": "^3.0.0", + "node-preload": "^0.2.1", + "p-map": "^3.0.0", + "process-on-spawn": "^1.0.0", + "resolve-from": "^5.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "spawn-wrap": "^2.0.0", + "test-exclude": "^6.0.0", + "yargs": "^15.0.2" + }, + "bin": { + "nyc": "bin/nyc.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nyc/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": ">=8" + } + }, + "node_modules/nyc/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/nyc/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/nyc/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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/nyc/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nyc/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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/nyc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/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/nyc/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/nyc/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nyc/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-on-spawn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz", + "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fromentries": "^1.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/release-zalgo": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", + "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", + "dev": true, + "license": "ISC", + "dependencies": { + "es6-error": "^4.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/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/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-wrap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/spawn-wrap/node_modules/foreground-child": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz", + "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/spawn-wrap/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/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "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/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", + "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.0", + "@typescript-eslint/parser": "8.59.0", + "@typescript-eslint/typescript-estree": "8.59.0", + "@typescript-eslint/utils": "8.59.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/write-file-atomic/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/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/nodejs/package.json b/nodejs/package.json new file mode 100644 index 0000000..f3107d6 --- /dev/null +++ b/nodejs/package.json @@ -0,0 +1,40 @@ +{ + "name": "@microsoft/antissrf", + "version": "1.0.0", + "description": "A library to prevent SSRF vulnerbilities in Node.js applications", + "main": "./out/src/index.js", + "types": "./out/src/index.d.ts", + "files": [ + "out/src/**/*", + "CHANGELOG.md" + ], + "scripts": { + "build": "tsc --project ./tsconfig.json", + "format": "prettier --write .", + "lint": "eslint src", + "test": "mocha --recursive --timeout 15000 --require ts-node/register --no-strip-types \"tests/{UnitTests,FunctionalTests}/**/*.test.ts\"", + "test:coverage": "nyc mocha --recursive --timeout 15000 --require ts-node/register tests/**/*.test.ts", + "test:unit": "mocha --recursive --timeout 15000 --require ts-node/register tests/UnitTests/**/*.test.ts", + "test:functional": "mocha --recursive --timeout 15000 --require ts-node/register --no-strip-types tests/FunctionalTests/**/*.test.ts", + "test:prepublish": "mocha --recursive --timeout 15000 tests/PrePublishTests/**/*.test.js" + }, + "author": "Microsoft", + "devDependencies": { + "@eslint/js": "^9.20.0", + "@types/follow-redirects": "^1.14.4", + "@types/mocha": "^10.0.10", + "@types/node-fetch": "^2.6.13", + "axios": "^1.12.2", + "eslint": "^9.20.0", + "eslint-plugin-security": "^3.0.1", + "follow-redirects": "^1.15.9", + "mocha": "^11.7.5", + "node-fetch": "^3.3.2", + "nyc": "^17.1.0", + "prettier": "^3.5.3", + "tar": "^7.4.3", + "ts-node": "^10.9.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0" + } +} diff --git a/nodejs/src/AntiSSRFError.ts b/nodejs/src/AntiSSRFError.ts new file mode 100644 index 0000000..09c3137 --- /dev/null +++ b/nodejs/src/AntiSSRFError.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export class AntiSSRFError extends Error {} diff --git a/nodejs/src/AntiSSRFPolicy.ts b/nodejs/src/AntiSSRFPolicy.ts new file mode 100644 index 0000000..1e6063e --- /dev/null +++ b/nodejs/src/AntiSSRFPolicy.ts @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ClientRequest, AgentOptions as HttpAgentOptions } from "http"; +import { AgentOptions as HttpsAgentOptions } from "https"; +import { BlockList } from "net"; + +import { AntiSSRFError, IPAddressRanges } from "."; +import { CIDRBlock } from "./Helpers/CIDRBlock"; +import { AntiSSRFHttpsAgent } from "./Helpers/AntiSSRFHttpsAgent"; +import { AntiSSRFHttpAgent } from "./Helpers/AntiSSRFHttpAgent"; + +export enum PolicyConfigOptions { + InternalOnly = "InternalOnly", + ExternalOnlyV1 = "ExternalOnlyV1", + ExternalOnlyLatest = "ExternalOnlyLatest", + None = "None" +} + +export class AntiSSRFPolicy { + // IP address related variables + private _allowedAddresses: BlockList; // maintains all networks are in IPv6 + private _deniedAddresses: BlockList; // maintains all networks are in IPv6 + private _denyAllUnspecifiedIPs: boolean; + + // Headers related variables + private _requiredHeaders: string[]; // maintains all headers are lowercase + private _deniedHeaders: string[]; // maintains all headers are lowercase + private _addXFFHeader: boolean = false; + private _allowPlainTextHttp: boolean = false; + + /** + * Creates a new AntiSSRF policy with the specified default configuration. + * + * @param config The policy configuration: + * - `InternalOnly`: Denies all connections by default. Use `addAllowedAddresses()` to permit specific ranges. + * - `ExternalOnlyV1`: Blocks recommendedV1 IP ranges. Adds the `X-Forwarded-For` header to requests when missing. + * - `ExternalOnlyLatest`: Blocks recommendedLatest IP ranges. Adds the `X-Forwarded-For` header to requests when missing. + * - `None`: No restrictions. + */ + constructor(config: PolicyConfigOptions) { + if (config == null) { + throw new AntiSSRFError("Null argument"); + } + + this._allowedAddresses = new BlockList(); + this._deniedAddresses = new BlockList(); + this._denyAllUnspecifiedIPs = false; + + this._requiredHeaders = []; + this._deniedHeaders = []; + this._addXFFHeader = false; + this._allowPlainTextHttp = false; + + switch (config) { + // Block all IPs by default. Users must add their intended internal ranges. + case PolicyConfigOptions.InternalOnly: + this._denyAllUnspecifiedIPs = true; + break; + // Block recommendedV1 IPs. Blocks IMDS, so add XFF. + case PolicyConfigOptions.ExternalOnlyV1: + this.addDeniedAddresses(IPAddressRanges.recommendedV1); + this._addXFFHeader = true; + break; + // Block recommendedLatest IPs. Blocks IMDS, so add XFF. + case PolicyConfigOptions.ExternalOnlyLatest: + this.addDeniedAddresses(IPAddressRanges.recommendedLatest); + this._addXFFHeader = true; + break; + // No restrictions. + case PolicyConfigOptions.None: + break; + default: + throw new AntiSSRFError("Argument must be a valid PolicyConfigOptions value"); + } + } + + /** + * ===== The IP addresses related functionality ===== + */ + + /** + * Gets or sets whether all unspecified IPs are denied by default. + * When true, only explicitly allowed addresses can connect. + * When false, addresses are evaluated against the denied addresses list. + */ + + get denyAllUnspecifiedIPs(): boolean { + return this._denyAllUnspecifiedIPs; + } + + set denyAllUnspecifiedIPs(value: boolean) { + if (value == null) { + throw new AntiSSRFError("Null argument"); + } + this._denyAllUnspecifiedIPs = value; + } + + /** + * Gets the allowed IP addresses BlockList (readonly). + */ + get allowedAddresses(): Readonly { + return this._allowedAddresses; + } + + /** + * Adds the specified IP addresses and/or range of IP addresses to the + * collection of allowed addresses. + * + * @param networks List of IPv4 and/or IPv6 addresses and/or subnets in + * CIDR notation + * @throws AntiSSRFError on improperly formatted network + */ + public addAllowedAddresses(networks: string[]): void { + if (networks == null) { + throw new AntiSSRFError("Null argument"); + } + + const parsedNetworks = networks.map((n) => CIDRBlock._parseCIDR(n)); + + for (const network of parsedNetworks) { + this._allowedAddresses.addSubnet(network.getAddress(), network.getPrefix(), "ipv6"); + } + } + + /** + * Gets the denied IP addresses BlockList (readonly). + */ + get deniedAddresses(): Readonly { + return this._deniedAddresses; + } + + /** + * Adds the specified IP addresses and/or range subnets to the collection + * of denied addresses. + * + * @param networks List of IPv4 and/or IPv6 addresses and/or subnets in + * CIDR notation + * @throws AntiSSRFError on improperly formatted network + */ + public addDeniedAddresses(networks: string[]): void { + if (networks == null) { + throw new AntiSSRFError("Null argument"); + } + + if (this._denyAllUnspecifiedIPs) { + throw new AntiSSRFError("Can't add denied networks when denyAllUnspecifiedIPs is true"); + } + + const parsedNetworks = networks.map((n) => CIDRBlock._parseCIDR(n)); + + for (const network of parsedNetworks) { + this._deniedAddresses.addSubnet(network.getAddress(), network.getPrefix(), "ipv6"); + } + } + + /** + * @internal + * This method is intended for internal use by AntiSSRF agents only. + * Use getHttpAgent() or getHttpsAgent() for public API. + */ + public _isNetworkConnectionAllowed(ipaddresses: string[]): boolean { + if (ipaddresses == null) { + return false; + } + + for (const ipaddress of ipaddresses) { + if (ipaddress == null) { + return false; + } + + try { + const [ipv6] = CIDRBlock._parseIPAddress(ipaddress); + + if (this._allowedAddresses.check(ipv6, "ipv6")) { + // If the address is in the allow list, it is allowed + continue; + } + + if (this._denyAllUnspecifiedIPs || this._deniedAddresses.check(ipv6, "ipv6")) { + // If the address is not in an allow list, it's not allowed + return false; + } + } catch { + return false; + } + } + + // No IP address was denied + return true; + } + + /** + * ===== The headers policy related functionality ===== + */ + + /** + * Gets the list of required headers (readonly copy). + */ + get requiredHeaders(): readonly string[] { + return [...this._requiredHeaders]; + } + + /** + * Adds headers to the collection of required headers. + * + * @param headers List of headers to require + * @throws AntiSSRFError on null/undefined headers array and on any + * null/undefined or empty string header + */ + public addRequiredHeaders(headers: string[]): void { + if (headers == null) { + throw new AntiSSRFError("Null argument"); + } + + for (const header of headers) { + if (header == null) { + throw new AntiSSRFError("Headers cannot be null or undefined"); + } + if (header.trim() === "") { + throw new AntiSSRFError("Headers cannot be an empty string"); + } + } + + this._requiredHeaders.push(...headers.map((h) => h.toLowerCase())); + } + + /** + * Gets the list of denied headers (readonly copy). + */ + get deniedHeaders(): readonly string[] { + return [...this._deniedHeaders]; + } + + /** + * Adds headers to the collection of denied headers. + * + * @param headers List of headers to deny + * @throws AntiSSRFError on null/undefined headers array and on any + * null/undefined or empty string header + */ + public addDeniedHeaders(headers: string[]): void { + if (headers == null) { + throw new AntiSSRFError("Null argument"); + } + + for (const header of headers) { + if (header == null) { + throw new AntiSSRFError("Headers cannot be null or undefined"); + } + if (header.trim() === "") { + throw new AntiSSRFError("Headers cannot be an empty string"); + } + } + + this._deniedHeaders.push(...headers.map((h) => h.toLowerCase())); + } + + /** + * Gets or sets whether to add the XFF header to all requests. + * True to add the XFF header to all requests, false otherwise. + */ + + get addXFFHeader(): boolean { + return this._addXFFHeader; + } + + set addXFFHeader(value: boolean) { + if (value == null) { + throw new AntiSSRFError("Null argument"); + } + this._addXFFHeader = value; + } + + /** + * Gets or sets whether to allow http or to require https. + * True to allow http, false to require https. + */ + + get allowPlainTextHttp(): boolean { + return this._allowPlainTextHttp; + } + + set allowPlainTextHttp(value: boolean) { + if (value == null) { + throw new AntiSSRFError("Null argument"); + } + this._allowPlainTextHttp = value; + } + + /** + * @internal + * This method is intended for internal use by AntiSSRF agents only. + * Use getHttpAgent() or getHttpsAgent() for public API. + */ + public _isHttpRequestAllowed(req: ClientRequest): boolean { + if (req == null || req.protocol == null) { + return false; + } + + // Node URL protocol property returns the protocol with the ':' + // Check if the protocol is plain text AND plain text is disallowed + if (!(req.protocol.toLowerCase() === "https:" || this._allowPlainTextHttp)) { + return false; + } + + // Node URL protocol property returns the protocol with the ':' + // Ensure the protocol is http(s) + if (!(req.protocol.toLowerCase() === "http:" || req.protocol.toLowerCase() === "https:")) { + return false; + } + + // Add the XFF header if required + if (this._addXFFHeader && !req.getHeaderNames().includes("x-forwarded-for")) { + req.setHeader("X-Forwarded-For", "true"); + } + + // Ensure none of the denied headers are present + for (const header of this._deniedHeaders) { + if (req.hasHeader(header)) { + return false; + } + } + + // Ensure all of the required headers are present + for (const header of this._requiredHeaders) { + if (!req.hasHeader(header)) { + return false; + } + } + + return true; + } + + /** + * ===== The Agent for easy policy enforcement ===== + */ + + public getHttpsAgent(options?: HttpsAgentOptions) { + return new AntiSSRFHttpsAgent(this, options); + } + + public getHttpAgent(options?: HttpAgentOptions) { + return new AntiSSRFHttpAgent(this, options); + } +} diff --git a/nodejs/src/Helpers/AntiSSRFDnsLookup.ts b/nodejs/src/Helpers/AntiSSRFDnsLookup.ts new file mode 100644 index 0000000..79d1a22 --- /dev/null +++ b/nodejs/src/Helpers/AntiSSRFDnsLookup.ts @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { lookup, LookupAddress, LookupAllOptions, LookupOneOptions } from "dns"; +import { LookupFunction } from "net"; + +import { AntiSSRFError, AntiSSRFPolicy } from ".."; + +class LookupWithPolicy { + private _policy: AntiSSRFPolicy; + + constructor(policy: AntiSSRFPolicy) { + this._policy = policy; + } + + private _lookupAll = ( + hostname: string, + options: LookupAllOptions, // options.all == true + callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void + ) => { + return lookup(hostname, options, (err, addresses) => { + // Errored in dns.lookup, forward error to callback + if (err != null) { + return callback(err, []); + } + + // This case should error in dns.lookup, so we should never hit this + /* istanbul ignore next */ + if (addresses == null) { + return callback(new AntiSSRFError("Error in DNS lookup"), []); + } + + // This case should never happen, since we only get to this function + // if options.all == true, but we are including this to satisfy + // typescript typechecking issues + /* istanbul ignore next */ + if (!(addresses instanceof Array)) { + return callback(new AntiSSRFError("Error in DNS lookup"), []); + } + + // Handle the case where no valid address was found + // [] instead of error for dns.lookup backwards compatibility + if (addresses.length === 0) { + return callback(null, []); + } + + // If all addresses are allowed by policy, forward to callback + if (this._policy._isNetworkConnectionAllowed(addresses.map((address) => address.address))) { + return callback(null, addresses); + } + + // If any address is disallowed by policy, return error + return callback(new AntiSSRFError("IP address disallowed by policy"), []); + }); + }; + + private _lookupOne = ( + hostname: string, + options: LookupOneOptions, + callback: (err: NodeJS.ErrnoException | null, address: string | LookupAddress[], family: number) => void + ) => { + return lookup(hostname, options, (err, address, family) => { + // Errored in dns.lookup, forward error to callback + if (err != null) { + return callback(err, null, family); + } + + // Handle the case where no valid address was found + // null instead of error for dns.lookup backwards compatibility + if (address == null) { + return callback(null, null, family); + } + + // If the address is allowed by policy, forward to callback + if (this._policy._isNetworkConnectionAllowed([address])) { + return callback(null, address, family); + } + + // If the address is disallowed by policy, return error + return callback(new AntiSSRFError("IP address disallowed by policy"), null, family); + }); + }; + + /** + * @internal + */ + public _lookup: LookupFunction = (hostname, options, callback) => { + if (options?.all == true) { + return this._lookupAll(hostname, options as LookupAllOptions, callback); + } else { + return this._lookupOne(hostname, options as LookupOneOptions, callback); + } + }; +} + +/** + * @internal + * Intended for internal use only. Use AntiSSRFPolicy agents for public use. + */ +export function antiSSRFDnsLookup(policy: AntiSSRFPolicy): LookupFunction { + if (policy == null) { + throw new AntiSSRFError("Null argument"); + } + + const lookupWithPolicy = new LookupWithPolicy(policy); + return lookupWithPolicy._lookup; +} diff --git a/nodejs/src/Helpers/AntiSSRFHttpAgent.ts b/nodejs/src/Helpers/AntiSSRFHttpAgent.ts new file mode 100644 index 0000000..1b406c5 --- /dev/null +++ b/nodejs/src/Helpers/AntiSSRFHttpAgent.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/** + * Any changes in this file need to be copied into AntiSSRFHttpsAgent.ts as well. + * The two files must always be the same, except that this one extends + * http.Agent while AntiSSRFHttpsAgent extends https.Agent. + * + * The two parent Agents have different default options and implement + * createConnection differently. + */ + +import { Agent as HttpAgent, AgentOptions as HttpAgentOptions, ClientRequest } from "http"; +import { isIP, LookupFunction } from "net"; + +import { antiSSRFDnsLookup } from "./AntiSSRFDnsLookup"; +import { AntiSSRFError, AntiSSRFPolicy } from "../"; + +export class AntiSSRFHttpAgent extends HttpAgent { + private _antiSSRFPolicy: AntiSSRFPolicy; + private _antiSSRFLookup: LookupFunction; + + constructor(policy: AntiSSRFPolicy, options?: HttpAgentOptions) { + // Ensure the user does not expect to use a different dns.lookup function + if (options?.lookup != null) { + throw new AntiSSRFError("Cannot use AntiSSRFHttpAgent with custom lookup function"); + } + + // Call the parent constructor + super(options); + + // Set custom variables + this._antiSSRFPolicy = policy; + this._antiSSRFLookup = antiSSRFDnsLookup(policy); + } + + /** + * This function is a wrapper around the addAgent function in http.Agent. + * It is used to check the headers portion of the policy and to add the + * XFF header if required. + */ + addRequest = (req: ClientRequest, ...args: any[]) => { + // Check if the request headers are allowed by the policy, and add the + // XFF header if required. + // Check if plaintext requests are allowed by the policy. + if (!this._antiSSRFPolicy._isHttpRequestAllowed(req)) { + process.nextTick(() => { + req.emit("error", new AntiSSRFError("Request headers or protocol disallowed by policy")); + }); + return; + } + + // @ts-expect-error 'addRequest' isn't defined in '@types/node' + return super.addRequest(req, ...args); // eslint-disable-line + }; + + /** + * This function is a wrapper around the createConnection function in + * http.Agent. It is used to check if the host is an IP address, and if so, + * to check if it is allowed by the policy. Then, it sets the lookup + * function for the request to our AntiSSRFDnsLookup function. + * + * This wrapper function is needed because the createConnection function in + * http.Agent first checks if isIP(host), and if so, it skips our + * policy-based lookup function. To make sure the policy is still checked + * when the host is an IP address, this wrapper function explicitly checks + * if the host is an allowed IP address before letting the http.Agent + * createConnection function to take over. + * + * Note: NodeJS has known, inconsistent type documentation for the Agent + * createConnection methods. In the Agent code, createConnection is only + * used with this signature. + */ + createConnection = (options: any, callback: any) => { + // Ensure the user does not expect to use a different dns.lookup function + if (options?.lookup != null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(new AntiSSRFError("Cannot use AntiSSRFHttpAgent with custom lookup function"), null); + } + + // http.request host is supposed to default to localhost. We want to + // ensure we know what the host is ahead of time for the policy check, + // so we are explicitly setting the default host if it is not already + // set. + // Not sure it is possible to get here, excluding from test cases + /* istanbul ignore next 5 */ + if (options == null) { + options = { host: "localhost" }; + } else if (options.host == null) { + options.host = "localhost"; + } + + // If host is an IP address, check if it is allowed by the policy. + if (isIP(options.host) && !this._antiSSRFPolicy._isNetworkConnectionAllowed([options.host])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(new AntiSSRFError("IP address disallowed by policy"), null); + } + + // Set the lookup function for the request to the AntiSSRFDnsLookup + // function. Since we created this agent with lookup = null and we + // checked that options.lookup = null, we can safely set it to our + // custom lookup function. + options.lookup = this._antiSSRFLookup; + + return super.createConnection(options, callback); + }; +} diff --git a/nodejs/src/Helpers/AntiSSRFHttpsAgent.ts b/nodejs/src/Helpers/AntiSSRFHttpsAgent.ts new file mode 100644 index 0000000..545d535 --- /dev/null +++ b/nodejs/src/Helpers/AntiSSRFHttpsAgent.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/** + * Any changes in this file need to be copied into AntiSSRFHttpAgent.ts as well. + * The two files must always be the same, except that this one extends + * https.Agent while AntiSSRFHttpAgent extends http.Agent. + * + * The two parent Agents have different default options and implement + * createConnection differently. + */ + +import { ClientRequest } from "http"; +import { Agent as HttpsAgent, AgentOptions as HttpsAgentOptions } from "https"; +import { isIP, LookupFunction } from "net"; + +import { antiSSRFDnsLookup } from "./AntiSSRFDnsLookup"; +import { AntiSSRFError, AntiSSRFPolicy } from "../"; + +export class AntiSSRFHttpsAgent extends HttpsAgent { + private _antiSSRFPolicy: AntiSSRFPolicy; + private _antiSSRFLookup: LookupFunction; + + constructor(policy: AntiSSRFPolicy, options?: HttpsAgentOptions) { + // Ensure the user does not expect to use a different dns.lookup function + if (options?.lookup != null) { + throw new AntiSSRFError("Cannot use AntiSSRFHttpsAgent with custom lookup function"); + } + + // Call the parent constructor + super(options); + + // Set custom variables + this._antiSSRFPolicy = policy; + this._antiSSRFLookup = antiSSRFDnsLookup(policy); + } + + /** + * This function is a wrapper around the addAgent function in https.Agent. + * It is used to check the headers portion of the policy and to add the + * XFF header if required. + */ + addRequest = (req: ClientRequest, ...args: any[]) => { + // Check if the request headers are allowed by the policy, and add the + // XFF header if required. + // Check if plaintext requests are allowed by the policy. + if (!this._antiSSRFPolicy._isHttpRequestAllowed(req)) { + process.nextTick(() => { + req.emit("error", new AntiSSRFError("Request headers or protocol disallowed by policy")); + }); + return; + } + + // @ts-expect-error 'addRequest' isn't defined in '@types/node' + return super.addRequest(req, ...args); // eslint-disable-line + }; + + /** + * This function is a wrapper around the createConnection function in + * https.Agent. It is used to check if the host is an IP address, and if so, + * to check if it is allowed by the policy. Then, it sets the lookup + * function for the request to our AntiSSRFDnsLookup function. + * + * This wrapper function is needed because the createConnection function in + * https.Agent first checks if isIP(host), and if so, it skips our + * policy-based lookup function. To make sure the policy is still checked + * when the host is an IP address, this wrapper function explicitly checks + * if the host is an allowed IP address before letting the https.Agent + * createConnection function to take over. + * + * Note: NodeJS has known, inconsistent type documentation for the Agent + * createConnection methods. In the Agent code, createConnection is only + * used with this signature. + */ + createConnection = (options: any, callback: any) => { + // Ensure the user does not expect to use a different dns.lookup function + if (options?.lookup != null) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(new AntiSSRFError("Cannot use AntiSSRFHttpsAgent with custom lookup function"), null); + } + + // https.request host is supposed to default to localhost. We want to + // ensure we know what the host is ahead of time for the policy check, + // so we are explicitly setting the default host if it is not already + // set. + // Not sure it is possible to get here, excluding from test cases + /* istanbul ignore next 5 */ + if (options == null) { + options = { host: "localhost" }; + } else if (options.host == null) { + options.host = "localhost"; + } + + // If host is an IP address, check if it is allowed by the policy. + if (isIP(options.host) && !this._antiSSRFPolicy._isNetworkConnectionAllowed([options.host])) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + return callback(new AntiSSRFError("IP address disallowed by policy"), null); + } + + // Set the lookup function for the request to the AntiSSRFDnsLookup + // function. Since we created this agent with lookup = null and we + // checked that options.lookup = null, we can safely set it to our + // custom lookup function. + options.lookup = this._antiSSRFLookup; + + return super.createConnection(options, callback); + }; +} diff --git a/nodejs/src/Helpers/CIDRBlock.ts b/nodejs/src/Helpers/CIDRBlock.ts new file mode 100644 index 0000000..db75322 --- /dev/null +++ b/nodejs/src/Helpers/CIDRBlock.ts @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { isIPv4, isIPv6, SocketAddress } from "net"; + +import { AntiSSRFError } from ".."; + +// Exactly 1 zero OR a non-zero decimal digit followed by 0-2 decimal digits (max 3 chars) +const decRegex = /^(0|[1-9][0-9]{0,2})$/; + +export class CIDRBlock { + private _address: string; + private _prefix: number; + + public getAddress(): string { + return this._address; + } + + public getPrefix(): number { + return this._prefix; + } + + /** + * @internal + * @throws AntiSSRFError If arguments are null or prefix is invalid + */ + constructor(address: string, prefix: number) { + if (address == null || prefix == null) { + throw new AntiSSRFError("Null argument"); + } + + if (isNaN(prefix) || prefix < 0 || prefix > 128) { + throw new AntiSSRFError("Invalid prefix"); + } + + if (!isIPv6(address)) { + throw new AntiSSRFError(`Invalid IPv6 address: ${address}`); + } + + this._address = address; + this._prefix = prefix; + } + + /** + * @internal + * @throws AntiSSRFError If the IP address is null or invalid + */ + public static _parseIPAddress(ipaddress: string): [string, 4 | 6] { + if (ipaddress == null) { + throw new AntiSSRFError("Null argument"); + } + + if (ipaddress.includes(":")) { + return [this._parseIPv6(ipaddress), 6]; + } else { + return [this._parseIPv4(ipaddress), 4]; + } + } + + /** + * @internal + * Returns a tuple with (IP address mapped to IPv6, the original IP version) + * @throws AntiSSRFError If the CIDR string is null, malformed, or contains invalid components + */ + public static _parseCIDR(cidr: string): CIDRBlock { + if (cidr == null) { + throw new AntiSSRFError("Null argument"); + } + + const parts = cidr.split("/"); + try { + const [ipv6, oldVersion] = this._parseIPAddress(parts[0]); + + if (parts.length == 1) { + return new CIDRBlock(ipv6, 128); + } else if (parts.length == 2) { + if (decRegex.test(parts[1])) { + const prefixLength = parseInt(parts[1], 10); + if (oldVersion === 4) { + // IPv4-mapped IPv6 address, adjust the prefix length accordingly + return new CIDRBlock(ipv6, prefixLength + 96); + } else { + return new CIDRBlock(ipv6, prefixLength); + } + } else { + throw new AntiSSRFError(`Invalid prefix length: ${parts[1]}`); + } + } else { + throw new AntiSSRFError(`Invalid CIDR block: ${cidr}`); + } + } catch { + throw new AntiSSRFError(`Invalid CIDR block: ${cidr}`); + } + } + + /** + * 1. x:x:x:x:x:x:x:x, where the 'x's are 1-4 hexadecimal digits + * 2. The same as (1) with exactly 1 :: to compress 1+ consecutive groups + * of 0s. + * 3. x:x:x:x:x:x:d.d.d.d, with or without compression in the xs, where the + * d.d.d.d is in standard IPv4 representation without leading 0s. + * 4. [IPv6], to support IPv6 as hostnames + */ + private static _parseIPv6(address: string): string { + // Strip brackets if the address is in the form [IPv6] + const len = address.length; + if (len > 2 && address[0] == "[" && address[len - 1] == "]") { + address = address.substring(1, len - 1); + } + + if (isIPv6(address)) { + try { + const socketAddress = new SocketAddress({ address, family: "ipv6" }); + return socketAddress.address; + } catch { + throw new AntiSSRFError(`Invalid IPv6 address: ${address}`); + } + } else { + throw new AntiSSRFError(`Invalid IPv6 address: ${address}`); + } + } + + /** + * Node.js requires IPv4 addresses to be in dotted-quad notation, with + * exactly 4 sections, without any leading 0s. + */ + private static _parseIPv4(address: string): string { + if (isIPv4(address)) { + return `::FFFF:${address}`; + } else { + throw new AntiSSRFError(`Invalid IPv4 address: ${address}`); + } + } +} diff --git a/nodejs/config/IPAddressRanges.ts b/nodejs/src/IPAddressRanges.ts similarity index 100% rename from nodejs/config/IPAddressRanges.ts rename to nodejs/src/IPAddressRanges.ts diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts new file mode 100644 index 0000000..f2e0815 --- /dev/null +++ b/nodejs/src/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from "./AntiSSRFError"; +export * from "./AntiSSRFPolicy"; +export * from "./IPAddressRanges"; diff --git a/nodejs/tests/FunctionalTests/AxiosDefaults.test.ts b/nodejs/tests/FunctionalTests/AxiosDefaults.test.ts new file mode 100644 index 0000000..3df87ad --- /dev/null +++ b/nodejs/tests/FunctionalTests/AxiosDefaults.test.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The Axios library allows you to set default http/https agents. + * + * This test suite sets Axios default agents with an AntiSSRFPolicy and tests + * various scenarios, including absolute addresses with and without redirects + * and absolute addresses that are redirected through a local HTTP server. + */ + +import axios from "axios"; +import assert from "assert"; +import { createServer, Server } from "http"; +import { lookup, promises } from "dns"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +const allowedAddressesNoRedirect = ["https://github.com/"]; + +const allowedAddressesWithRedirect = [ + "https://google.com/", + "http://localhost:3000/?redirectTo=https://github.com/", + "http://localhost:3000/?redirectTo=https://google.com/" +]; + +const deniedAddressesNoRedirect = [ + "https://apple.com/", + "https://www.facebook.com", + "https://www.bing.com", + "https://outlook.live.com", + "https://www.etsy.com", + "https://169.254.169.254/" +]; + +const deniedAddressesWithRedirect = [ + "http://localhost:3000/?redirectTo=https://apple.com/", + "http://localhost:3000/?redirectTo=https://www.facebook.com", + "http://localhost:3000/?redirectTo=https://www.bing.com", + "http://localhost:3000/?redirectTo=https://outlook.live.com", + "http://localhost:3000/?redirectTo=https://www.etsy.com", + "http://localhost:3000/?redirectTo=https://169.254.169.254/" +]; + +describe("Axios Defaults tests", () => { + let server: Server; + + /** + * Set up policy: + * - Add XFF header + * - Require header "test-required-header" + * - Deny header "test-denied-header" + * - Deny all unspecifieid addresses + * - Allow addresses from GitHub, Google, and NYT + * + * - Allow plain text HTTP required for local HTTP server + * - Allow localhost required for local HTTP server + */ + before(async () => { + // Set up policy + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.addXFFHeader = true; + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + policy.denyAllUnspecifiedIPs = true; + + // Set up allowed IPs + const githubIPs = await promises.lookup("github.com", { family: 0, all: true }); + const googleIPs = await promises.lookup("google.com", { family: 0, all: true }); + const moregoogleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + policy.addAllowedAddresses([...githubIPs, ...googleIPs, ...moregoogleIPs].map((ip) => ip.address)); + policy.addAllowedAddresses(["::1", "127.0.0.1"]); + + // Set up HTTP server to redirect requests + server = createServer((req, res) => { + const { url } = req; + // Ensure the request has the XFF header + assert.equal(req.headers["x-forwarded-for"], "true"); + res.writeHead(301, { Location: url?.substring(url.indexOf("=") + 1) ?? "" }); + res.end(); + }); + server.listen(3000); + + // Set the Axios default agents + axios.defaults.httpAgent = policy.getHttpAgent(); + axios.defaults.httpsAgent = policy.getHttpsAgent(); + }); + + describe("Allowed addresses that don't cause redirects", () => { + allowedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await axios.get(url, { + maxRedirects: 0, + headers: { "test-required-header": "true" } + }); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - no maxRedirects specified`, async () => { + try { + const res = await axios.get(url, { headers: { "test-required-header": "true" } }); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + }); + }); + + describe("Allowed addresses that cause redirects", () => { + allowedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await axios.get(url, { + maxRedirects: 0, + headers: { "test-required-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request failed with status code 301"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await axios.get(url, { headers: { "test-required-header": "true" } }); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + }); + }); + + describe("Denied addresses that don't cause redirects", () => { + deniedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await axios.get(url, { + maxRedirects: 0, + headers: { "test-required-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await axios.get(url, { headers: { "test-required-header": "true" } }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + }); + }); + + describe("Denied addresses that cause redirects", () => { + deniedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await axios.get(url, { + maxRedirects: 0, + headers: { "test-required-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request failed with status code 301"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await axios.get(url, { headers: { "test-required-header": "true" } }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + }); + }); + + it("Contains denied header", async () => { + try { + const res = await axios.get("https://github.com", { + headers: { "test-required-header": "true", "test-denied-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request headers or protocol disallowed by policy"); + } + }); + + it("Missing required header", async () => { + try { + const res = await axios.get("https://github.com", { + headers: {} + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request headers or protocol disallowed by policy"); + } + }); + + it("Tries to overwrite lookup", async () => { + try { + const res = await axios.get("https://github.com", { + headers: { "test-required-header": "true" }, + // @ts-ignore Testing that custom lookup is rejected + lookup: lookup + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Cannot use AntiSSRFHttpsAgent with custom lookup function"); + } + }); + + after(() => { + axios.defaults.httpAgent = undefined; + axios.defaults.httpsAgent = undefined; + + server.close(); + }); +}); diff --git a/nodejs/tests/FunctionalTests/AxiosInstance.test.ts b/nodejs/tests/FunctionalTests/AxiosInstance.test.ts new file mode 100644 index 0000000..bc152f1 --- /dev/null +++ b/nodejs/tests/FunctionalTests/AxiosInstance.test.ts @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * The Axios library allows you to create an instance with custom configuration, + * which can include custom headers, base URLs, Agents, timeouts, interceptors, + * and more. MaxRedirects controls if Axios should use the http/https libraries + * or the follow-redirects library. + * + * This test suite create an Axios instance with an AntiSSRFPolicy and tests + * various scenarios, including absolute addresses with and without redirects, + * absolute addresses that are redirected through a local HTTP server, and + * relative addresses that use the base URL feature. + */ + +import axios, { AxiosInstance } from "axios"; +import assert from "assert"; +import { createServer, Server } from "http"; +import { lookup, promises } from "dns"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +const allowedAddressesNoRedirect = ["https://github.com/"]; + +const allowedAddressesWithRedirect = [ + "https://google.com/", + "http://localhost:3000/?redirectTo=https://github.com/", + "http://localhost:3000/?redirectTo=https://google.com/" +]; + +const deniedAddressesNoRedirect = [ + "https://apple.com/", + "https://www.facebook.com", + "https://www.bing.com", + "https://outlook.live.com", + "https://www.etsy.com", + "https://169.254.169.254/" +]; + +const deniedAddressesWithRedirect = [ + "http://localhost:3000/?redirectTo=https://apple.com/", + "http://localhost:3000/?redirectTo=https://www.facebook.com", + "http://localhost:3000/?redirectTo=https://www.bing.com", + "http://localhost:3000/?redirectTo=https://outlook.live.com", + "http://localhost:3000/?redirectTo=https://www.etsy.com", + "http://localhost:3000/?redirectTo=https://169.254.169.254/" +]; + +describe("Axios Instance tests", () => { + let server: Server; + let instance: AxiosInstance; + + /** + * Set up policy: + * - Add XFF header + * - Require header "test-required-header" + * - Deny header "test-denied-header" + * - Deny all unspecifieid addresses + * - Allow addresses from GitHub, Google, and NYT + * + * - Allow plain text HTTP required for local HTTP server + * - Allow localhost required for local HTTP server + */ + before(async () => { + // Set up policy + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.addXFFHeader = true; + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + policy.denyAllUnspecifiedIPs = true; + + // Set up allowed IPs + const githubIPs = await promises.lookup("github.com", { family: 0, all: true }); + const googleIPs = await promises.lookup("google.com", { family: 0, all: true }); + const moregoogleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + policy.addAllowedAddresses([...githubIPs, ...googleIPs, ...moregoogleIPs].map((ip) => ip.address)); + policy.addAllowedAddresses(["::1", "127.0.0.1"]); + + // Set up HTTP server to redirect requests + server = createServer((req, res) => { + const { url } = req; + // Ensure the request has the XFF header + assert.equal(req.headers["x-forwarded-for"], "true"); + res.writeHead(301, { Location: url?.substring(url.indexOf("=") + 1) ?? "" }); + res.end(); + }); + server.listen(3000); + + // Create the Axios instance + instance = axios.create({ + headers: { "test-required-header": "true" }, + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + baseURL: "https://google.com" + }); + }); + + describe("Allowed addresses that don't cause redirects", () => { + allowedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await instance.get(url, { + maxRedirects: 0 + }); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - no maxRedirects specified`, async () => { + try { + const res = await instance.get(url); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + }); + }); + + describe("Allowed addresses that cause redirects", () => { + allowedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await instance.get(url, { + maxRedirects: 0 + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request failed with status code 301"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await instance.get(url); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + }); + }); + + describe("Denied addresses that don't cause redirects", () => { + deniedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await instance.get(url, { + maxRedirects: 0 + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await instance.get(url); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + }); + }); + + describe("Denied addresses that cause redirects", () => { + deniedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - maxRedirects = 0`, async () => { + try { + const res = await instance.get(url, { + maxRedirects: 0 + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request failed with status code 301"); + } + }); + + it(`GET ${url} - no maxRedirects`, async () => { + try { + const res = await instance.get(url); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "IP address disallowed by policy"); + } + }); + }); + }); + + it("Contains denied header", async () => { + try { + const res = await instance.get("https://github.com", { + headers: { "test-required-header": "true", "test-denied-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request headers or protocol disallowed by policy"); + } + }); + + it("Tries to overwrite lookup", async () => { + try { + const res = await instance.get("https://github.com", { + headers: { "test-required-header": "true" }, + lookup: lookup + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Cannot use AntiSSRFHttpsAgent with custom lookup function"); + } + }); + + it("Uses baseURL", async () => { + try { + const res = await instance.get("/", { + headers: { "test-required-header": "true" } + }); + assert.ok(res.status == 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it("Incorrectly uses baseURL", async () => { + try { + const res = await instance.get("www.bing.com", { + headers: { "test-required-header": "true" } + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, "Request failed with status code 404"); + } + }); + + after(() => { + server.close(); + }); +}); diff --git a/nodejs/tests/FunctionalTests/FollowRedirects.test.ts b/nodejs/tests/FunctionalTests/FollowRedirects.test.ts new file mode 100644 index 0000000..3a9f8cb --- /dev/null +++ b/nodejs/tests/FunctionalTests/FollowRedirects.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Library: https://github.com/follow-redirects/follow-redirects + * + * Description: follow-redirects provides request and get methods that behave + * identically to those found on the native http and https modules, with the + * exception that they will seamlessly follow redirects. + * + * Notes: Used by Axios whenever maxRedirects != 0. + */ + +import assert from "assert"; +import { createServer, Server, Agent as NodeHttpAgent } from "http"; +import { Agent as NodeHttpsAgent } from "https"; +import { promises } from "dns"; +import { http, https } from "follow-redirects"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("Follow-Redirects Library Tests", () => { + describe("Redirect proxy", () => { + let server: Server; + let httpAgent: NodeHttpAgent; + let httpsAgent: NodeHttpsAgent; + + before(async () => { + // Set up server to redirect requests + server = createServer((req, res) => { + const { url } = req; + assert.equal(req.headers["x-forwarded-for"], "true"); + res.writeHead(301, { Location: url?.substring(url.indexOf("=") + 1) ?? "" }); + res.end(); + }); + server.listen(3000); + + // Set up policy + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.addXFFHeader = true; + policy.denyAllUnspecifiedIPs = true; + + // Set up allowed IPs + const githubIPs = await promises.lookup("github.com", { family: 0, all: true }); + const googleIPs = await promises.lookup("google.com", { family: 0, all: true }); + const moreGoogleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + const portalAzureIPs = await promises.lookup("portal.azure.com", { family: 0, all: true }); + policy.addAllowedAddresses( + [...githubIPs, ...googleIPs, ...moreGoogleIPs, ...portalAzureIPs].map((ip) => ip.address) + ); + policy.addAllowedAddresses(["::1", "127.0.0.1"]); + + httpAgent = policy.getHttpAgent({ keepAlive: false }); + httpsAgent = policy.getHttpsAgent({ keepAlive: false }); + }); + + const allowedRedirects = [ + "https://github.com/", + "https://google.com/", + "https://portal.azure.com/", + "http://localhost:3000/?redirectTo=https://google.com/" + ]; + allowedRedirects.forEach((url) => { + it(`GET http://localhost:3000/?redirectTo=${url}`, (done) => { + const req = http.get( + `http://localhost:3000/?redirectTo=${url}`, + { agents: { http: httpAgent, https: httpsAgent } }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + if (!url.includes("google")) { + assert.equal(url, res.responseUrl); + } + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + const disallowedRedirects = ["https://apple.com/", "https://cmu.edu/", "https://www.bing.com/"]; + disallowedRedirects.forEach((url) => { + it(`GET http://localhost:3000/?redirectTo=${url}`, (done) => { + const req = http.get( + `http://localhost:3000/?redirectTo=${url}`, + { trackRedirects: true, agents: { http: httpAgent, https: httpsAgent } }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + }); + + after(() => { + server.close(); + httpAgent.destroy(); + httpsAgent.destroy(); + }); + }); +}); diff --git a/nodejs/tests/FunctionalTests/HttpAgent.test.ts b/nodejs/tests/FunctionalTests/HttpAgent.test.ts new file mode 100644 index 0000000..a41f797 --- /dev/null +++ b/nodejs/tests/FunctionalTests/HttpAgent.test.ts @@ -0,0 +1,486 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import http from "http"; +import { lookup, LookupAddress, promises } from "dns"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("HttpAgent Tests - default policy", () => { + let antiSSRFHttpAgent: http.Agent; + let microsoftIP: string; + + before(async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + policy.allowPlainTextHttp = true; + antiSSRFHttpAgent = policy.getHttpAgent({ keepAlive: true }); + microsoftIP = await promises.lookup("microsoft.com", { family: 4 }).then((address) => address.address); + }); + + it("Successful lookup - get, URL", (done) => { + const req = http.get("http://www.apple.com/", { agent: antiSSRFHttpAgent, family: 4 }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - get, options", (done) => { + const req = http.get( + { + agent: antiSSRFHttpAgent, + host: microsoftIP, + headers: { Host: "microsoft.com" } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 307); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, URL", (done) => { + const req = http.request( + "http://twin-cities.umn.edu/academics-admissions/majors-programs", + { agent: antiSSRFHttpAgent }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, options", (done) => { + const req = http.request( + { + agent: antiSSRFHttpAgent, + hostname: "learn.microsoft.com", + path: "/en-us/training/paths/describe-basic-concepts-of-cybersecurity/" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Reject lookup - get, URL", (done) => { + const req = http.get("http://[0::1]/", { agent: antiSSRFHttpAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - get, options", (done) => { + const req = http.get({ agent: antiSSRFHttpAgent, hostname: "127.0.0.3", host: "google.com" }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - request, URL", (done) => { + const req = http.request( + "http://169.254.169.254:443", + { + agent: antiSSRFHttpAgent, + host: "www.google.com" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - request, options", (done) => { + const req = http.request( + { + agent: antiSSRFHttpAgent, + host: "www.imds.michaelhendrickx.com" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "getaddrinfo ENOTFOUND www.imds.michaelhendrickx.com"); + done(); + }); + + req.end(); + }); + + it("Successful lookup - any, ensure XFF is added", (done) => { + const req = http.request("http://www.apple.com/", { agent: antiSSRFHttpAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(() => { + assert.equal(req.getHeader("X-Forwarded-For"), "true"); + }); + }); + + it("Successful lookup - any, ensure XFF is not overwritten", (done) => { + const req = http.request( + "http://www.apple.com/", + { agent: antiSSRFHttpAgent, headers: { "X-Forwarded-For": "127.0.0.1" } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(() => { + assert.equal(req.getHeader("x-forwarded-for"), "127.0.0.1"); + }); + }); + + it("Reject lookup - tried to add lookup to request", (done) => { + const req = http.get("http://google.com", { agent: antiSSRFHttpAgent, lookup: lookup }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "Cannot use AntiSSRFHttpAgent with custom lookup function"); + done(); + }); + + req.end(); + }); + + after(() => { + antiSSRFHttpAgent.destroy(); + }); +}); + +describe("HttpAgent Tests - custom policy", () => { + let antiSSRFHttpAgent: http.Agent; + let googleIPs: LookupAddress[]; + let appleIPs: LookupAddress[]; + + before(async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + + googleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + appleIPs = await promises.lookup("apple.com", { family: 0, all: true }); + policy.addDeniedAddresses([...googleIPs, ...appleIPs].map((address) => address.address)); + + antiSSRFHttpAgent = policy.getHttpAgent(); + }); + + it("Successful lookup - get, URL", (done) => { + const req = http.get( + "http://www.bing.com/", + { agent: antiSSRFHttpAgent, headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - get, options", (done) => { + const req = http.get( + { agent: antiSSRFHttpAgent, hostname: "github.com", headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, URL", (done) => { + const req = http.get( + "http://www.cmu.edu/", + { agent: antiSSRFHttpAgent, headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, options", (done) => { + const req = http.request( + { + agent: antiSSRFHttpAgent, + hostname: "learn.microsoft.com", + path: "/en-us/training/paths/describe-basic-concepts-of-cybersecurity/", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Reject lookup - get, URL", (done) => { + const req = http.get( + "http://apple.com", + { + agent: antiSSRFHttpAgent, + host: "www.bing.com", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - get, options", (done) => { + const req = http.get( + { + agent: antiSSRFHttpAgent, + host: appleIPs.find((address) => address.family === 4)?.address, + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - request, options", (done) => { + const req = http.request( + { + agent: antiSSRFHttpAgent, + host: "www.google.com", + family: 0, + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, missing required header", (done) => { + const req = http.get("http://www.google.com/", { agent: antiSSRFHttpAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "Request headers or protocol disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, include denied header", (done) => { + const req = http.get( + "http://www.bing.com/", + { agent: antiSSRFHttpAgent, headers: { "test-required-header": "true", "test-denied-header": "false" } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "Request headers or protocol disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, tried to overwrite lookup", (done) => { + // @ts-expect-error - trying to overwrite lookup should cause error + antiSSRFHttpAgent.lookup = lookup; + + const req = http.get( + "http://apple.com", + { + agent: antiSSRFHttpAgent, + host: "www.google.com", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Bad agent construction", () => { + assert.throws(() => { + const newPolicy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const newAgent = newPolicy.getHttpAgent({ lookup: lookup }); + }); + }); + + after(() => { + antiSSRFHttpAgent.destroy(); + }); +}); diff --git a/nodejs/tests/FunctionalTests/HttpsAgent.test.ts b/nodejs/tests/FunctionalTests/HttpsAgent.test.ts new file mode 100644 index 0000000..8f28c76 --- /dev/null +++ b/nodejs/tests/FunctionalTests/HttpsAgent.test.ts @@ -0,0 +1,780 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import https from "https"; +import { lookup, LookupAddress, promises } from "dns"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("HttpsAgent Tests - default policy", () => { + let antiSSRFHttpsAgent: https.Agent; + let microsoftIP: string; + + before(async () => { + antiSSRFHttpsAgent = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest).getHttpsAgent({ + keepAlive: true + }); + microsoftIP = await promises.lookup("microsoft.com", { family: 4 }).then((address) => address.address); + }); + + it("Successful lookup - get, URL", (done) => { + const req = https.get("https://www.apple.com/", { agent: antiSSRFHttpsAgent, family: 4 }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - get, options", (done) => { + const req = https.get( + { + agent: antiSSRFHttpsAgent, + host: microsoftIP, + servername: "microsoft.com", + headers: { Host: "microsoft.com" } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 301); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, URL", (done) => { + const req = https.request( + "https://twin-cities.umn.edu/academics-admissions/majors-programs", + { agent: antiSSRFHttpsAgent }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, options", (done) => { + const req = https.request( + { + agent: antiSSRFHttpsAgent, + hostname: "learn.microsoft.com", + path: "/en-us/training/paths/describe-basic-concepts-of-cybersecurity/" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Reject lookup - get, URL", (done) => { + const req = https.get("https://[0::1]/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - get, options", (done) => { + const req = https.get({ agent: antiSSRFHttpsAgent, hostname: "127.0.0.3", host: "google.com" }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - request, URL", (done) => { + const req = https.request( + "https://169.254.169.254:443", + { + agent: antiSSRFHttpsAgent, + host: "www.google.com" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - request, options", (done) => { + const req = https.request( + { + agent: antiSSRFHttpsAgent, + host: "www.imds.michaelhendrickx.com" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "getaddrinfo ENOTFOUND www.imds.michaelhendrickx.com"); + done(); + }); + + req.end(); + }); + + it("Successful lookup - any, ensure XFF is added", (done) => { + const req = https.request("https://www.apple.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(() => { + assert.equal(req.getHeader("X-Forwarded-For"), "true"); + }); + }); + + it("Successful lookup - any, ensure XFF is not overwritten", (done) => { + const req = https.request( + "https://www.apple.com/", + { agent: antiSSRFHttpsAgent, headers: { "X-Forwarded-For": "127.0.0.1" } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(() => { + assert.equal(req.getHeader("x-forwarded-for"), "127.0.0.1"); + }); + }); + + it("Reject lookup - tried to add lookup to request", (done) => { + const req = https.get("https://google.com", { agent: antiSSRFHttpsAgent, lookup: lookup }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "Cannot use AntiSSRFHttpsAgent with custom lookup function"); + done(); + }); + + req.end(); + }); + + after(() => { + antiSSRFHttpsAgent.destroy(); + }); +}); + +describe("HttpsAgent Tests - custom policy", () => { + let antiSSRFHttpsAgent: https.Agent; + let googleIPs: LookupAddress[]; + let appleIPs: LookupAddress[]; + + before(async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + + googleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + appleIPs = await promises.lookup("apple.com", { family: 0, all: true }); + policy.addDeniedAddresses([...googleIPs, ...appleIPs].map((address) => address.address)); + + antiSSRFHttpsAgent = policy.getHttpsAgent(); + }); + + it("Successful lookup - get, URL", (done) => { + const req = https.get( + "https://www.bing.com/", + { agent: antiSSRFHttpsAgent, port: 443, headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - get, options", (done) => { + const req = https.get( + { agent: antiSSRFHttpsAgent, hostname: "github.com", headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + if (res.statusCode == 200) { + done(); + } else { + done(new Error(`Expected 200, got ${res.statusCode}`)); + } + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, URL", (done) => { + const req = https.get( + "https://www.cmu.edu/", + { agent: antiSSRFHttpsAgent, headers: { "test-required-header": 25 } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Successful lookup - request, options", (done) => { + const req = https.request( + { + agent: antiSSRFHttpsAgent, + hostname: "learn.microsoft.com", + path: "/en-us/training/paths/describe-basic-concepts-of-cybersecurity/", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Reject lookup - get, URL", (done) => { + const req = https.get( + "https://apple.com", + { + agent: antiSSRFHttpsAgent, + host: "www.bing.com", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - get, options", (done) => { + const req = https.get( + { + agent: antiSSRFHttpsAgent, + host: appleIPs.find((address) => address.family === 4)?.address, + headers: { "test-required-header": 25 }, + servername: "apple.com" + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + // Azure Pipeline not supporting IPv6 + // it("Reject lookup - request, URL", (done) => { + // const req = https.request( + // `https://[${googleIPs.find((address) => address.family === 6).address}]:443`, + // { + // agent: antiSSRFHttpsAgent, + // host: "google.com", + // family: 4, + // headers: { "test-required-header": 25 } + // }, + // (res) => { + // res.on("data", (data) => {}); + // res.on("end", () => { + // done("Expected error, but got response"); + // }); + // } + // ); + + // req.on("error", (err) => { + // assert.equal(err.message, "IP address disallowed by policy"); + // done(); + // }); + + // req.end(); + // }); + + it("Reject lookup - request, options", (done) => { + const req = https.request( + { + agent: antiSSRFHttpsAgent, + host: "www.google.com", + family: 0, + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, missing required header", (done) => { + const req = https.get("https://www.google.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", (err) => { + assert.equal(err.message, "Request headers or protocol disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, include denied header", (done) => { + const req = https.get( + "https://www.bing.com/", + { agent: antiSSRFHttpsAgent, headers: { "test-required-header": "true", "test-denied-header": "false" } }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "Request headers or protocol disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Reject lookup - any, tried to overwrite lookup", (done) => { + // @ts-expect-error - trying to overwrite lookup should cause error + antiSSRFHttpsAgent.lookup = lookup; + + const req = https.get( + "https://apple.com", + { + agent: antiSSRFHttpsAgent, + host: "www.google.com", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + it("Bad agent construction", () => { + assert.throws(() => { + const newPolicy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const newAgent = newPolicy.getHttpsAgent({ lookup: lookup }); + }); + }); + + after(() => { + antiSSRFHttpsAgent.destroy(); + }); +}); + +describe("HttpsAgent Tests - other methods", () => { + const testUrl = "https://ambitious-flower-0611c910f.2.azurestaticapps.net/api/method"; + let allowAgent: https.Agent; + let disallowAgent: https.Agent; + + before(async () => { + const allowPolicy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + allowAgent = allowPolicy.getHttpsAgent(); + + const disallowPolicy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + const disallowedIPs = await promises.lookup(new URL(testUrl).hostname, { family: 0, all: true }); + disallowPolicy.addDeniedAddresses(disallowedIPs.map((addr) => addr.address)); + disallowAgent = disallowPolicy.getHttpsAgent(); + }); + + const methods = ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"]; + methods.map((method) => { + it(`${method} allowed`, (done) => { + const req = https.request(testUrl, { method, agent: allowAgent }, (res) => { + let responseData = ""; + res.on("data", (chunk) => { + responseData += chunk.toString(); + }); + res.on("end", () => { + if (method === "HEAD") { + assert.equal(res.statusCode, 200); + return done(); + } + + try { + assert.equal(res.statusCode, 200); + const parsedData = JSON.parse(responseData); + assert.equal(parsedData.method, method); + done(); + } catch (error) { + done(error); + } + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it(`${method} disallowed`, (done) => { + const req = https.request(testUrl, { method, agent: disallowAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + }); + + req.on("error", () => { + done(); + }); + + req.end(); + }); + }); + + after(() => { + allowAgent.destroy(); + disallowAgent.destroy(); + }); +}); + +describe("HttpsAgent Tests - certificates", () => { + let antiSSRFHttpsAgent: https.Agent; + + before(() => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + antiSSRFHttpsAgent = policy.getHttpsAgent(); + }); + + describe("Valid Certificate Tests", () => { + it("Valid certificate should succeed", (done) => { + const req = https.get("https://www.google.com", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + }); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + describe("Expired Certificate Tests", () => { + it("Expired certificate should fail with certificate verification enabled", (done) => { + const req = https.get("https://expired.badssl.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + done(new Error("Expected SSL error, but got response")); + }); + }); + + req.on("error", (err: any) => { + assert.ok(err.message.includes("certificate") || err.code === "CERT_HAS_EXPIRED"); + done(); + }); + + req.end(); + }); + + it("Expired certificate should succeed with certificate verification disabled", (done) => { + const req = https.get( + "https://expired.badssl.com/", + { agent: antiSSRFHttpsAgent, rejectUnauthorized: false }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + describe("Wrong Host Certificate Tests", () => { + it("Wrong host certificate should fail with certificate verification enabled", (done) => { + const req = https.get("https://wrong.host.badssl.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + done(new Error("Expected SSL error, but got response")); + }); + }); + + req.on("error", (err: any) => { + assert.ok( + err.message.includes("certificate") || + err.message.includes("Hostname/IP does not match") || + err.code === "ERR_TLS_CERT_ALTNAME_INVALID" + ); + done(); + }); + + req.end(); + }); + + it("Wrong host certificate should succeed with certificate verification disabled", (done) => { + const req = https.get( + "https://wrong.host.badssl.com/", + { agent: antiSSRFHttpsAgent, rejectUnauthorized: false }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + describe("Self-Signed Certificate Tests", () => { + it("Self-signed certificate should fail with certificate verification enabled", (done) => { + const req = https.get("https://self-signed.badssl.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + done(new Error("Expected SSL error, but got response")); + }); + }); + + req.on("error", (err: any) => { + assert.ok( + err.message.includes("certificate") || + err.message.includes("self-signed") || + err.code === "DEPTH_ZERO_SELF_SIGNED_CERT" + ); + done(); + }); + + req.end(); + }); + + it("Self-signed certificate should succeed with certificate verification disabled", (done) => { + const req = https.get( + "https://self-signed.badssl.com/", + { agent: antiSSRFHttpsAgent, rejectUnauthorized: false }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + describe("Untrusted Root Certificate Tests", () => { + it("Untrusted root certificate should fail with certificate verification enabled", (done) => { + const req = https.get("https://untrusted-root.badssl.com/", { agent: antiSSRFHttpsAgent }, (res) => { + res.on("data", () => {}); + res.on("end", () => { + done(new Error("Expected SSL error, but got response")); + }); + }); + + req.on("error", (err: any) => { + assert.ok( + err.message.includes("certificate") || + err.message.includes("unable to verify") || + err.code === "UNABLE_TO_VERIFY_LEAF_SIGNATURE" + ); + done(); + }); + + req.end(); + }); + + it("Untrusted root certificate should succeed with certificate verification disabled", (done) => { + const req = https.get( + "https://untrusted-root.badssl.com/", + { agent: antiSSRFHttpsAgent, rejectUnauthorized: false }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + }); + + after(() => { + antiSSRFHttpsAgent.destroy(); + }); +}); diff --git a/nodejs/tests/FunctionalTests/NodeFetch.test.ts b/nodejs/tests/FunctionalTests/NodeFetch.test.ts new file mode 100644 index 0000000..b70b0e5 --- /dev/null +++ b/nodejs/tests/FunctionalTests/NodeFetch.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * Library: https://github.com/node-fetch/node-fetch/tree/2.x#readme + * + * Description: The node-fetch library provides a window.fetch compatible API for making + * HTTP requests in Node.js. It only accepts absolute URLs, without any support for + * relative URLs or protocol-relative URLs. It allows for a custom agent function to + * choose the agent for the correct protocol automatically. + */ + +import assert from "assert"; +import http, { createServer, Server } from "http"; +import https from "https"; +import { promises } from "dns"; +const fetch = require("node-fetch").default; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +const allowedAddressesNoRedirect = ["https://github.com/"]; + +const allowedAddressesWithRedirect = [ + "https://google.com/", + "http://localhost:3000/?redirectTo=https://github.com/", + "http://localhost:3000/?redirectTo=https://google.com/" +]; + +const deniedAddressesNoRedirect = [ + "https://apple.com/", + "https://www.facebook.com", + "https://www.bing.com", + "https://outlook.live.com", + "https://www.etsy.com", + "https://169.254.169.254/" +]; + +const deniedAddressesWithRedirect = [ + "http://localhost:3000/?redirectTo=https://apple.com/", + "http://localhost:3000/?redirectTo=https://www.facebook.com", + "http://localhost:3000/?redirectTo=https://www.bing.com", + "http://localhost:3000/?redirectTo=https://outlook.live.com", + "http://localhost:3000/?redirectTo=https://www.etsy.com", + "http://localhost:3000/?redirectTo=https://169.254.169.254/" +]; + +describe("Node-Fetch Tests", () => { + let server: Server; + let httpAgent: http.Agent; + let httpsAgent: https.Agent; + let agentFn: (parsedURL: URL) => http.Agent | https.Agent; + + /** + * Set up policy: + * - Add XFF header + * - Require header "test-required-header" + * - Deny header "test-denied-header" + * - Deny all unspecified addresses + * - Allow addresses from GitHub, Google + * + * - Allow plain text HTTP required for local HTTP server + * - Allow localhost required for local HTTP server + */ + before(async () => { + // Set up policy + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.addXFFHeader = true; + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + policy.denyAllUnspecifiedIPs = true; + + // Set up allowed IPs + const githubIPs = await promises.lookup("github.com", { family: 0, all: true }); + const googleIPs = await promises.lookup("google.com", { family: 0, all: true }); + const moregoogleIPs = await promises.lookup("www.google.com", { family: 0, all: true }); + policy.addAllowedAddresses([...githubIPs, ...googleIPs, ...moregoogleIPs].map((ip) => ip.address)); + policy.addAllowedAddresses(["::1", "127.0.0.1"]); + + // Set up HTTP server to redirect requests + server = createServer((req, res) => { + const { url } = req; + // Ensure the request has the XFF header + assert.equal(req.headers["x-forwarded-for"], "true"); + res.writeHead(301, { Location: url?.substring(url.indexOf("=") + 1) }); + res.end(); + }); + server.listen(3000); + + // Get agents from policy + httpAgent = policy.getHttpAgent({ keepAlive: false }); + httpsAgent = policy.getHttpsAgent({ keepAlive: false }); + agentFn = (_parsedURL) => { + return _parsedURL.protocol === "https:" ? httpsAgent : httpAgent; + }; + }); + + describe("Allowed addresses that don't cause redirects", () => { + allowedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - follow = 0`, async () => { + try { + const res = await fetch(url, { + follow: 0, + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "manual"`, async () => { + try { + const res = await fetch(url, { + redirect: "manual", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "follow"`, async () => { + try { + const res = await fetch(url, { + redirect: "follow", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "error"`, async () => { + try { + const res = await fetch(url, { + redirect: "error", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 200); + } catch (err) { + assert.fail(err as Error); + } + }); + }); + }); + + describe("Allowed addresses that cause redirects", () => { + allowedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - follow = 0`, async () => { + try { + const res = await fetch(url, { + follow: 0, + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, `maximum redirect reached at: ${url}`); + } + }); + + it(`GET ${url} - redirect = "manual"`, async () => { + try { + const res = await fetch(url, { + redirect: "manual", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 301); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "follow"`, async () => { + try { + const res = await fetch(url, { + redirect: "follow", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 200); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "error"`, async () => { + try { + const res = await fetch(url, { + redirect: "error", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal( + (err as Error).message, + `uri requested responds with a redirect, redirect mode is set to error: ${url}` + ); + } + }); + }); + }); + + describe("Denied addresses that don't cause redirects", () => { + deniedAddressesNoRedirect.forEach((url) => { + it(`GET ${url} - follow = 0`, async () => { + try { + const res = await fetch(url, { + follow: 0, + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("IP address disallowed by policy")); + } + }); + + it(`GET ${url} - redirect = "manual"`, async () => { + try { + const res = await fetch(url, { + redirect: "manual", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("IP address disallowed by policy")); + } + }); + + it(`GET ${url} - redirect = "follow"`, async () => { + try { + const res = await fetch(url, { + redirect: "follow", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("IP address disallowed by policy")); + } + }); + + it(`GET ${url} - redirect = "error"`, async () => { + try { + const res = await fetch(url, { + redirect: "error", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("IP address disallowed by policy")); + } + }); + }); + }); + + describe("Denied addresses that cause redirects", () => { + deniedAddressesWithRedirect.forEach((url) => { + it(`GET ${url} - follow = 0`, async () => { + try { + const res = await fetch(url, { + follow: 0, + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal((err as Error).message, `maximum redirect reached at: ${url}`); + } + }); + + it(`GET ${url} - redirect = "manual"`, async () => { + try { + const res = await fetch(url, { + redirect: "manual", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.ok(res.status === 301); + } catch (err) { + assert.fail(err as Error); + } + }); + + it(`GET ${url} - redirect = "follow"`, async () => { + try { + const res = await fetch(url, { + redirect: "follow", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("IP address disallowed by policy")); + } + }); + + it(`GET ${url} - redirect = "error"`, async () => { + try { + const res = await fetch(url, { + redirect: "error", + headers: { "test-required-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.equal( + (err as Error).message, + `uri requested responds with a redirect, redirect mode is set to error: ${url}` + ); + } + }); + }); + }); + + describe("Header policy enforcement", () => { + it("Contains denied header", async () => { + try { + const res = await fetch("https://github.com", { + headers: { "test-required-header": "true", "test-denied-header": "true" }, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("Request headers or protocol disallowed by policy")); + } + }); + + it("Missing required header", async () => { + try { + const res = await fetch("https://github.com", { + headers: {}, + agent: agentFn + }); + assert.fail("Expected error, but got response"); + } catch (err) { + assert.ok((err as Error).message.includes("Request headers or protocol disallowed by policy")); + } + }); + }); + + after(() => { + httpAgent.destroy(); + httpsAgent.destroy(); + server.close(); + }); +}); diff --git a/nodejs/tests/PrePublishTests/PrePublish.test.js b/nodejs/tests/PrePublishTests/PrePublish.test.js new file mode 100644 index 0000000..6eacf6e --- /dev/null +++ b/nodejs/tests/PrePublishTests/PrePublish.test.js @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const fs = require("fs"); +const path = require("path"); +const tar = require("tar"); +const assert = require("assert"); +const dns = require("dns"); +const https = require("https"); + +describe("Tests for most recent .tgz package", function () { + const baseDir = path.join(__dirname, "../.."); + const extractPath = path.join(baseDir, "temp-lib"); + let URIValidate; + let AntiSSRFPolicy; + + before(async function () { + // Find the .tgz file dynamically + const files = fs.readdirSync(baseDir); + const tgzFile = files.find((file) => file.match(/^.*\.tgz$/)); + + if (!tgzFile) { + throw new Error("No matching .tgz file found."); + } + + const tgzPath = path.join(baseDir, tgzFile); // Ensure the temp directory exists + + if (!fs.existsSync(extractPath)) { + fs.mkdirSync(extractPath); + } // Extract the .tgz file + + await tar.x({ + file: tgzPath, + cwd: extractPath, + sync: true, + strip: 1 + }); // Dynamically require the AddOne function + + const lib = require(path.join(extractPath, "out/src/index.js")); // Adjust if needed + URIValidate = lib.URIValidate; + AntiSSRFPolicy = lib.AntiSSRFPolicy; + }); + + it("URIValidate.inDomain test", () => { + assert.equal(URIValidate.inDomain("https://example.com", ".example.com"), true); + assert.equal(URIValidate.inDomain("https://example.com.evil.com", "example.com"), false); + }); + + describe("AntiSSRFPolicy tests", () => { + let antiSSRFHttpsAgent; + + before(async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["test-required-header"]); + policy.addDeniedHeaders(["test-denied-header"]); + + googleIPs = await dns.promises.lookup("www.google.com", { + family: 0, + all: true + }); + appleIPs = await dns.promises.lookup("apple.com", { family: 0, all: true }); + policy.addDeniedAddresses([...googleIPs, ...appleIPs].map((address) => address.address)); + + antiSSRFHttpsAgent = policy.getHttpsAgent(); + }); + + it("Allow valid IP", (done) => { + const req = https.get( + "https://www.bing.com", + { + agent: antiSSRFHttpsAgent, + port: 443, + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + assert.equal(res.statusCode, 200); + done(); + }); + } + ); + + req.on("error", (err) => { + done(err); + }); + + req.end(); + }); + + it("Deny blocked IP", (done) => { + const req = https.get( + "https://apple.com", + { + agent: antiSSRFHttpsAgent, + host: "www.bing.com", + headers: { "test-required-header": 25 } + }, + (res) => { + res.on("data", (data) => {}); + res.on("end", () => { + done("Expected error, but got response"); + }); + } + ); + + req.on("error", (err) => { + assert.equal(err.message, "IP address disallowed by policy"); + done(); + }); + + req.end(); + }); + + after(() => { + antiSSRFHttpsAgent.destroy(); + }); + }); +}); diff --git a/nodejs/tests/UnitTests/AntiSSRFDnsLookup.test.ts b/nodejs/tests/UnitTests/AntiSSRFDnsLookup.test.ts new file mode 100644 index 0000000..99ec3bf --- /dev/null +++ b/nodejs/tests/UnitTests/AntiSSRFDnsLookup.test.ts @@ -0,0 +1,492 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import { ADDRCONFIG, ALL, lookup, LookupAddress, LookupOptions, V4MAPPED } from "dns"; +import { LookupFunction } from "net"; + +import { antiSSRFDnsLookup } from "../../src/Helpers/AntiSSRFDnsLookup"; +import { AntiSSRFPolicy, AntiSSRFError, PolicyConfigOptions } from "../../src"; + +/** + * Converts a callback-based lookup function to a promise-based lookup function + * for easier testing. + */ +const customPromisify = (lookup: LookupFunction) => (hostname: string, options: LookupOptions) => + new Promise((resolve, reject) => { + lookup(hostname, options, (err, address, family) => { + if (err) { + return reject(err); + } + if (family != null) { + return resolve({ address: address as string, family }); + } else { + return resolve(address as LookupAddress[]); + } + }); + }); + +const optionsToString = (options: LookupOptions | null | undefined) => { + if (!options) { + return options; + } + + return ( + "{" + + Object.entries(options) + .map(([key, value]) => { + return `${key}: ${value}`; + }) + .join(", ") + + "}" + ); +}; + +/** + * Asserts that the result of AntiSSRFDnsLookup matches the result of dns.lookup + * for the given hostname and options. + */ +const AssertMatchResult = async (policy: AntiSSRFPolicy, hostname: string, options: LookupOptions) => { + let expected; + try { + expected = await customPromisify(lookup)(hostname, options); + } catch (err) { + assert.fail( + `Expected dns.lookup to not error: hostname - ${hostname}, options - ${optionsToString(options)}, error - ${err}` + ); + } + + let actual; + try { + actual = await customPromisify(antiSSRFDnsLookup(policy))(hostname, options); + } catch (err) { + assert.fail( + `Expected AntiSSRFDnsLookup to not error: hostname - ${hostname}, options - ${optionsToString(options)}, error - ${err}` + ); + } + + // If both are arrays, compare them as sets (same elements, order doesn't matter) + if (Array.isArray(actual) && Array.isArray(expected)) { + const sortedActual = [...actual].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + const sortedExpected = [...expected].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + assert.deepStrictEqual( + sortedActual, + sortedExpected, + `Expected results to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } else if ( + actual && + expected && + typeof actual === "object" && + typeof expected === "object" && + "address" in actual && + "family" in actual && + "address" in expected && + "family" in expected && + (actual as any).address !== (expected as any).address && + (actual as any).family === (expected as any).family + ) { + // If families match but addresses differ, warn instead of fail + console.warn( + `Address mismatch (family match): hostname - ${hostname}, expected address - ${(expected as any).address}, actual address - ${(actual as any).address}, family - ${(actual as any).family}, options - ${optionsToString(options)}` + ); + } else { + assert.deepStrictEqual( + actual, + expected, + `Expected results to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } +}; + +/** + * Asserts that the error from AntiSSRFDnsLookup matches the error from + * dns.lookup for the given hostname and options. + */ +const AssertMatchError = async (policy: AntiSSRFPolicy, hostname: string, options: LookupOptions) => { + let expectedError: Error | null = null; + try { + await customPromisify(lookup)(hostname, options); + } catch (err) { + expectedError = err as Error; + } + if (!expectedError) { + assert.fail(`Expected dns.lookup to error: hostname - ${hostname}, options - ${optionsToString(options)}`); + } + + let actualError: Error | null = null; + try { + await customPromisify(antiSSRFDnsLookup(policy))(hostname, options); + } catch (err) { + actualError = err as Error; + } + if (!actualError) { + assert.fail( + `Expected AntiSSRFDnsLookup to error: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } + + assert.deepStrictEqual( + actualError, + expectedError, + `Expected errors to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); +}; + +/** + * Asserts that the error from AntiSSRFDnsLookup matches the error from + * dns.lookup for the given hostname and options OR asserts that the result of + * AntiSSRFDnsLookup matches the result of dns.lookup for the given hostname and + * options. + * + * Only used for hostnames/options that behave difference between local and + * Azure pipeline tests. + */ +const AssertMatchResultOrError = async (policy: AntiSSRFPolicy, hostname: string, options: LookupOptions) => { + let expectedResult; + let expectedError: Error | null = null; + try { + expectedResult = await customPromisify(lookup)(hostname, options); + } catch (err) { + expectedError = err as Error; + } + + let actualResult; + let actualError: Error | null = null; + try { + actualResult = await customPromisify(antiSSRFDnsLookup(policy))(hostname, options); + } catch (err) { + actualError = err as Error; + } + + if (expectedError) { + assert.deepStrictEqual( + actualError, + expectedError, + `Expected errors to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } else { + if (Array.isArray(actualResult) && Array.isArray(expectedResult)) { + const sortedActual = [...actualResult].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + const sortedExpected = [...expectedResult].sort((a, b) => + JSON.stringify(a).localeCompare(JSON.stringify(b)) + ); + assert.deepStrictEqual( + sortedActual, + sortedExpected, + `Expected results to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } else if ( + actualResult && + expectedResult && + typeof actualResult === "object" && + typeof expectedResult === "object" && + "address" in actualResult && + "family" in actualResult && + "address" in expectedResult && + "family" in expectedResult && + (actualResult as any).address !== (expectedResult as any).address && + (actualResult as any).family === (expectedResult as any).family + ) { + // If families match but addresses differ, warn instead of fail + console.warn( + `Address mismatch (family match): hostname - ${hostname}, expected address - ${(expectedResult as any).address}, actual address - ${(actualResult as any).address}, family - ${(actualResult as any).family}, options - ${optionsToString(options)}` + ); + } else { + assert.deepStrictEqual( + actualResult, + expectedResult, + `Expected results to match: hostname - ${hostname}, options - ${optionsToString(options)}` + ); + } + } +}; + +describe("AntiSSRFDnsLookup", () => { + describe("Bad inputs", () => { + it("Null hostname", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + await AssertMatchResult(policy, null as unknown as string, {}); + await AssertMatchResult(policy, null as unknown as string, { all: false }); + await AssertMatchResult(policy, null as unknown as string, { all: true }); + await AssertMatchResult(policy, null as unknown as string, { family: 0 }); + await AssertMatchResult(policy, null as unknown as string, { family: 6 }); + }); + + it("Undefined hostname", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + await AssertMatchResult(policy, undefined as unknown as string, {}); + await AssertMatchResult(policy, undefined as unknown as string, { all: false }); + await AssertMatchResult(policy, undefined as unknown as string, { all: true }); + await AssertMatchResult(policy, undefined as unknown as string, { family: 4 }); + await AssertMatchResult(policy, undefined as unknown as string, { family: 6 }); + await AssertMatchResult(policy, undefined as unknown as string, { family: 0, all: true }); + }); + + it("Generally bad hostname", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + await AssertMatchError(policy, "hello", {}); + await AssertMatchError(policy, "https://google.com", { all: false }); + await AssertMatchError(policy, "https://www.google.com", null as any); + await AssertMatchError(policy, "google.com:60", { all: true }); + await AssertMatchError(policy, "google.com/path", { family: 4 }); + await AssertMatchError(policy, "google.com/search?q=hi", { family: 6 }); + await AssertMatchError(policy, "username@sup.com", { family: 0, all: true }); + await AssertMatchError(policy, "#fragment", null as any); + }); + + it("Bad options", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const hostname = "bing.com"; + + // options.all must be true or false + await AssertMatchError(policy, hostname, { all: 1 as unknown as boolean }); + + // options.family must by 0, 4, 6, "IPv4", or "IPv6" + await AssertMatchError(policy, hostname, { family: 3 as unknown as 0 }); + await AssertMatchError(policy, hostname, { family: "IPv0" as unknown as "IPv4" }); + + // options.hints can only be specific flags + await AssertMatchError(policy, hostname, { hints: -1 }); + + // options.order can only be "verbatim", "ipv4first", or "ipv6first" + // Behavior different across environments + await AssertMatchResultOrError(policy, hostname, { order: "NotAnOrder" as unknown as "verbatim" }); + + // options.verbatim can only be true or false + await AssertMatchError(policy, hostname, { verbatim: 1 as unknown as boolean }); + }); + }); + + /** + * All addresses are allowed, so dns.lookup and AntiSSRFDnsLookup should + * always return the same result or throw the same error. + */ + describe("Lookup with accepting policy", () => { + // If all addresses are allowed, dns.lookup and AntiSSRFDnsLookup should be the same + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + + const OPT_FAMILY: (0 | 4 | 6 | "IPv4" | "IPv6")[] = [4, 6, 0, "IPv4", "IPv6", undefined as unknown as 0]; + const OPT_ALL: boolean[] = [true, false, undefined as unknown as boolean]; + const OPT_ORDER: ("verbatim" | "ipv4first" | "ipv6first")[] = [ + "verbatim", + "ipv4first", + "ipv6first", + undefined as unknown as "verbatim" + ]; + const OPT_HINTS: number[] = [ + V4MAPPED, // 2048 + ALL, // 256 + ADDRCONFIG, // 1024 + V4MAPPED | ALL, + V4MAPPED | ADDRCONFIG, + ALL | ADDRCONFIG, + V4MAPPED | ALL | ADDRCONFIG, + undefined as unknown as number + ]; + const OPT_VERBATIM: boolean[] = [true, false, undefined as unknown as boolean]; + + const hostnames = ["google.com", "bing.com", "learn.microsoft.com"]; + for (const hostname of hostnames) { + it(`Common domain tests - ${hostname}`, async () => { + for (const all of OPT_ALL) { + for (const family of OPT_FAMILY) { + for (const order of OPT_ORDER) { + for (const hints of OPT_HINTS) { + for (const verbatim of OPT_VERBATIM) { + if (family == 6 || family == "IPv6") { + await AssertMatchResultOrError(policy, hostname, { + all, + family, + order, + hints, + verbatim + }); + } else { + await AssertMatchResult(policy, hostname, { + all, + family, + order, + hints, + verbatim + }); + } + } + } + } + } + } + }); + } + + const hostnames2 = ["azure.com", "github.com"]; + for (const hostname of hostnames2) { + it(`Common domain tests, no IPv6 - ${hostname}`, async () => { + for (const all of OPT_ALL) { + for (const family of OPT_FAMILY) { + for (const order of OPT_ORDER) { + for (const hints of OPT_HINTS) { + for (const verbatim of OPT_VERBATIM) { + if (family == 6 || family == "IPv6") { + await AssertMatchResultOrError(policy, hostname, { + all, + family, + order, + hints, + verbatim + }); + } else { + await AssertMatchResult(policy, hostname, { + all, + family, + order, + hints, + verbatim + }); + } + } + } + } + } + } + }); + } + }); + + describe("Lookup with policy functionality", () => { + it("Default policy", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + const promisifiedAntiSSRFLookup = customPromisify(antiSSRFDnsLookup(policy)); + + // Allowed by policy + await AssertMatchResult(policy, "google.com", { family: 4 }); + await AssertMatchResult(policy, "yAhOo.com", { family: 4, all: false }); + + // Disallowed by policy - IMDS + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("169.254.169.254", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("0xA9.0Xfe.0xA9.0xFe", { family: 4 }), + (err: Error) => { + if (process.platform === 'win32') { + return err.message.includes("getaddrinfo ENOTFOUND 0xA9.0Xfe.0xA9.0xFe"); + } else { + return err.message === "IP address disallowed by policy"; + } + } + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("169.254.169.254", { family: 6 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("::FFFF:169.254.169.254", { family: 4 }), + AntiSSRFError + ); + + // Rejects with different error in local vs Azure pipeline + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("imds.michaelhendrickx.com", { family: 0, all: true }) + ); + + // Disallowed by policy - WireServer + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("168.63.129.16", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("0xA8.0X3F.0x81.0x10", { family: 4 }), + (err: Error) => { + if (process.platform === 'win32') { + return err.message.includes("getaddrinfo ENOTFOUND 0xA8.0X3F.0x81.0x10"); + } else { + return err.message === "IP address disallowed by policy"; + } + } + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("168.63.129.16", { family: 6 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("::FFFF:168.63.129.16", { family: 4 }), + AntiSSRFError + ); + + // Disallowed by policy - localhost + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("localhost", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("localhost", { family: 6 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("127.0.0.1", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("127.0.0.1", { family: 6 }), + AntiSSRFError + ); + + // Disallowed by policy - other + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("100.64.0.10", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("100.64.0.10", { family: 6 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("::FFFF:100.64.0.10", { family: 4 }), + AntiSSRFError + ); + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("::FFFF:100.64.0.10", { family: 6 }), + AntiSSRFError + ); + + // More allowed by policy + await AssertMatchResult(policy, "bing.com", { family: 4 }); + await AssertMatchResult(policy, "microsoft.com", { family: 0 }); + await AssertMatchResult(policy, "docs.github.com", { family: 0, all: true }); + await AssertMatchResult(policy, "223.6.7.8", { family: 0, all: true }); + await AssertMatchResult(policy, "::fffF:223.6.7.8", { family: 0, all: true }); + }); + + it("addAllowedAddresses", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + const promisifiedAntiSSRFLookup = customPromisify(antiSSRFDnsLookup(policy)); + + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("169.254.0.2", { all: true }), + AntiSSRFError + ); + + policy.addAllowedAddresses(["169.254.0.2"]); + + await AssertMatchResult(policy, "169.254.0.2", null as any); + await AssertMatchResult(policy, "google.com", null as any); + }); + + it("addDeniedAddresses", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const promisifiedAntiSSRFLookup = customPromisify(antiSSRFDnsLookup(policy)); + + await AssertMatchResult(policy, "192.168.0.0", null as any); + + policy.addDeniedAddresses(["192.168.0.0"]); + + await assert.rejects( + async () => await promisifiedAntiSSRFLookup("192.168.0.0", null as any), + AntiSSRFError + ); + await AssertMatchResult(policy, "google.com", null as any); + }); + }); +}); diff --git a/nodejs/tests/UnitTests/AntiSSRFPolicy.AddXFFHeader.test.ts b/nodejs/tests/UnitTests/AntiSSRFPolicy.AddXFFHeader.test.ts new file mode 100644 index 0000000..5b9feba --- /dev/null +++ b/nodejs/tests/UnitTests/AntiSSRFPolicy.AddXFFHeader.test.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import axios from "axios"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("AntiSSRFPolicy AddXFFHeader Tests", () => { + const TEST_DOMAIN = "ambitious-flower-0611c910f.2.azurestaticapps.net"; + + it("check defaults", () => { + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.InternalOnly).addXFFHeader, false); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1).addXFFHeader, true); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest).addXFFHeader, true); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.None).addXFFHeader, false); + }); + + it("on true", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addXFFHeader = true; + + const res = await axios.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Forwarded-For`); + assert.strictEqual(res.status, 200); + }); + + it("does not overwrite header", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addXFFHeader = true; + + const res = await axios.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Forwarded-For`, { + headers: { + "X-Forwarded-For": "1.2.3.4" + } + }); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data.headerValue.includes("1.2.3.4"), true); + }); +}); diff --git a/nodejs/tests/UnitTests/AntiSSRFPolicy.Address.test.ts b/nodejs/tests/UnitTests/AntiSSRFPolicy.Address.test.ts new file mode 100644 index 0000000..c52bf5c --- /dev/null +++ b/nodejs/tests/UnitTests/AntiSSRFPolicy.Address.test.ts @@ -0,0 +1,820 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import axios from "axios"; +import { promises } from "dns"; + +import { AntiSSRFError, AntiSSRFPolicy, IPAddressRanges, PolicyConfigOptions } from "../../src"; + +describe("AntiSSRFPolicy Address Tests", () => { + const BAD_IP_MESSAGE = "IP address disallowed by policy"; + const TEST_DOMAIN = "ambitious-flower-0611c910f.2.azurestaticapps.net"; + + let instance1: axios.AxiosInstance; + let instance2: axios.AxiosInstance; + let instance3: axios.AxiosInstance; + let instance4: axios.AxiosInstance; + + before(() => { + const policy1 = new AntiSSRFPolicy(PolicyConfigOptions.InternalOnly); + policy1.allowPlainTextHttp = true; + instance1 = axios.create({ + httpAgent: policy1.getHttpAgent({ keepAlive: false }), + httpsAgent: policy1.getHttpsAgent({ keepAlive: false }) + }); + + const policy2 = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + policy2.allowPlainTextHttp = true; + instance2 = axios.create({ + httpAgent: policy2.getHttpAgent({ keepAlive: false }), + httpsAgent: policy2.getHttpsAgent({ keepAlive: false }) + }); + + const policy3 = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest); + policy3.allowPlainTextHttp = true; + instance3 = axios.create({ + httpAgent: policy3.getHttpAgent({ keepAlive: false }), + httpsAgent: policy3.getHttpsAgent({ keepAlive: false }) + }); + + const policy4 = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy4.allowPlainTextHttp = true; + instance4 = axios.create({ + httpAgent: policy4.getHttpAgent({ keepAlive: false }), + httpsAgent: policy4.getHttpsAgent({ keepAlive: false }), + timeout: 1, + signal: AbortSignal.timeout(1) + }); + }); + + it("bad inputs", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.denyAllUnspecifiedIPs = true; + assert.throws( + () => policy.addDeniedAddresses(["1.2.3.4"]), + AntiSSRFError, + "Expected addDeniedAddresses to throw when denyAllUnspecifiedIPs is true" + ); + + // Test null arrays + const policy2 = new AntiSSRFPolicy(PolicyConfigOptions.None); + assert.throws( + () => policy2.addAllowedAddresses(null as any), + AntiSSRFError, + "Expected addAllowedAddresses(null) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addAllowedAddresses([null as any]), + AntiSSRFError, + "Expected addAllowedAddresses([null]) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addAllowedAddresses(undefined as any), + AntiSSRFError, + "Expected addAllowedAddresses(undefined) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addAllowedAddresses([undefined as any]), + AntiSSRFError, + "Expected addAllowedAddresses([undefined]) to throw AntiSSRFError" + ); + assert.throws( + () => policy2.addDeniedAddresses(null as any), + AntiSSRFError, + "Expected addDeniedAddresses(null) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addDeniedAddresses([null as any]), + AntiSSRFError, + "Expected addDeniedAddresses([null]) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addDeniedAddresses(undefined as any), + AntiSSRFError, + "Expected addDeniedAddresses(undefined) to throw AntiSSRFError" + ); + assert.throws( + () => policy.addDeniedAddresses([undefined as any]), + AntiSSRFError, + "Expected addDeniedAddresses([undefined]) to throw AntiSSRFError" + ); + + // Test empty arrays + policy2.addAllowedAddresses([]); + policy2.addDeniedAddresses([]); + + // Test invalid IP address formats + assert.throws( + () => policy2.addDeniedAddresses(["invalid.ip.address"]), + AntiSSRFError, + "Expected addDeniedAddresses to throw for invalid IP format" + ); + assert.throws( + () => policy2.addDeniedAddresses(["256.256.256.256/24"]), + AntiSSRFError, + "Expected addDeniedAddresses to throw for out-of-range IPv4 values" + ); + assert.throws( + () => policy2.addDeniedAddresses(["192.168.1.1/33"]), + AntiSSRFError, + "Expected addDeniedAddresses to throw for invalid IPv4 prefix length" + ); + assert.throws( + () => policy2.addAllowedAddresses(["not-an-ip"]), + AntiSSRFError, + "Expected addAllowedAddresses to throw for invalid IP format" + ); + + // Test array containing null addresses + const policy3 = new AntiSSRFPolicy(PolicyConfigOptions.None); + assert.throws( + () => policy3.addDeniedAddresses(["192.168.1.0/24", null!, "10.0.0.0/8"]), + AntiSSRFError, + "Expected addDeniedAddresses to throw when address list contains null" + ); + assert.throws( + () => policy3.addAllowedAddresses([null!]), + AntiSSRFError, + "Expected addAllowedAddresses to throw when address list contains null" + ); + }); + + it("check defaults IMDS", async () => { + const urls = [ + "https://169.254.169.254/latest/meta-data/", + "https://0xA9.0xFE.0xA9.0xFE/latest/meta-data/", + "https://[::ffff:169.254.169.254]/latest/meta-data/", + "https://[::ffff:A9FE:A9FE]/latest/meta-data/", + `https://${TEST_DOMAIN}/api/imds-ip?code=301`, + `https://${TEST_DOMAIN}/api/imds-ip?code=302`, + `https://${TEST_DOMAIN}/api/imds?redirectNum=3` + ]; + + for (const url of urls) { + await assert.rejects( + async () => { + await instance1.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance2.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance3.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + try { + await instance4.get(url); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: ${url} but got ${err}` + ); + } + } + }).timeout(10000); + + it("check defaults wireserver", async () => { + const urls = [ + "http://168.63.129.16/", + "http://0xA8.0x3F.0x81.0x10/", + "http://[::ffff:168.63.129.16]/", + "http://[::ffff:A83F:8110]/", + `https://${TEST_DOMAIN}/api/wireserver` + ]; + + for (const url of urls) { + await assert.rejects( + async () => { + await instance1.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance2.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance3.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + try { + await instance4.get(url); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: ${url} but got ${err}` + ); + } + } + }).timeout(10000); + + it("check defaults localhost", async () => { + const urls = [ + "http://127.0.0.1/", + "http://0x7F.0x0.0x0.0x1/", + "http://[::ffff:127.0.0.1]/", + "http://[::ffff:7F00:1]/", + `https://${TEST_DOMAIN}/api/localhost`, + "http://localhost/" + ]; + + for (const url of urls) { + await assert.rejects( + async () => { + await instance1.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance2.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + await assert.rejects( + async () => { + await instance3.get(url); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected AntiSSRFError for URL: ${url}` + ); + try { + await instance4.get(url); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: ${url} but got ${err}` + ); + } + } + }).timeout(10000); + + it("default with IpAddressRanges", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + policy.addAllowedAddresses([ + ...IPAddressRanges.imds, + ...IPAddressRanges.wireserver, + ...IPAddressRanges.loopback + ]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true, + timeout: 1, + signal: AbortSignal.timeout(1) + }); + + try { + await instance.get(`http://${TEST_DOMAIN}/api/localhost`); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://${TEST_DOMAIN}/api/localhost but got ${err}` + ); + } + + try { + await instance.get(`http://${TEST_DOMAIN}/api/wireserver`); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://${TEST_DOMAIN}/api/wireserver but got ${err}` + ); + } + + try { + await instance.get(`http://${TEST_DOMAIN}/api/imds`); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://${TEST_DOMAIN}/api/imds but got ${err}` + ); + } + + try { + await instance.get("http://127.0.0.1"); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://127.0.0.1 but got ${err}` + ); + } + + try { + await instance.get("http://168.63.129.16"); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://168.63.129.16 but got ${err}` + ); + } + + try { + await instance.get("http://169.254.169.254"); + } catch (err) { + assert.notEqual( + (err as Error).message, + BAD_IP_MESSAGE, + `Expected non-AntiSSRFError for URL: http://169.254.169.254 but got ${err}` + ); + } + }); + + it("allow IPv4 addresses", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.denyAllUnspecifiedIPs = true; + const testIpArr = await promises.lookup(TEST_DOMAIN, { family: 4, all: true }); + policy.addAllowedAddresses(testIpArr.map((ip) => ip.address)); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Allowed IPv4 - allowed by policy + await assert.doesNotReject(async () => { + await instance.get(`http://${testIpArr[0].address}`); + }); + + // Allowed IPv4-mapped IPv6 - allowed by policy but might fail on some systems due to IPv6 handling + try { + await instance.get(`http://[::ffff:${testIpArr[0].address}]:80`); + } catch (err) { + assert.notEqual((err as Error).message, BAD_IP_MESSAGE); + } + + // Disallowed IPv4 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("http://1.2.3.4"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected request to disallowed IPv4 address to be rejected" + ); + + // Disallowed IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("http://[1:2:3:4:5:6:7:8]"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected request to disallowed IPv6 address to be rejected" + ); + }); + + it("allow IPv4-mapped IPv6 addresses", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.denyAllUnspecifiedIPs = true; + const testIpArr = await promises.lookup(TEST_DOMAIN, { family: 4, all: true }); + policy.addAllowedAddresses(testIpArr.map((ip) => `::ffff:${ip.address}`)); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Allowed IPv4 - allowed by policy + await assert.doesNotReject(async () => { + await instance.get("http://" + testIpArr[0].address); + }); + + // Allowed IPv4-mapped IPv6 - allowed by policy but might fail on some systems due to IPv6 handling + try { + await instance.get(`http://[::ffff:${testIpArr[0].address}]:80`); + } catch (err) { + assert.notEqual((err as Error).message, BAD_IP_MESSAGE); + } + + // Disallowed IPv4 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("http://1.2.3.4"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected request to disallowed IPv4 address to be rejected" + ); + + // Disallowed IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("http://[1:2:3:4:5:6:7:8]"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected request to disallowed IPv6 address to be rejected" + ); + }); + + it("allow IPv6 addresses", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + policy.denyAllUnspecifiedIPs = true; + const testIPv6 = "::1"; + policy.addAllowedAddresses([testIPv6]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Allowed IPv6 - allowed by policy but might fail on some systems due to IPv6 handling + try { + await instance.get(`http://[${testIPv6}]`); + } catch (err) { + assert.notEqual((err as Error).message, BAD_IP_MESSAGE); + } + + // Disallowed IPv4 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("https://1.2.3.4"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected disallowed IPv4 request to be rejected when only IPv6 is allowed" + ); + + // Disallowed different IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get("https://[2606:4700:4700::1111]"); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + "Expected disallowed IPv6 request to be rejected when only ::1 is allowed" + ); + }); + + it("deny IPv4 address", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const testIpArr = await promises.lookup(TEST_DOMAIN, { family: 4, all: true }); + policy.addDeniedAddresses(testIpArr.map((ip) => ip.address)); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Denied IPv4 - not allowed by policy + await assert.rejects( + async () => { + await instance.get(`https://${testIpArr[0].address}`); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected denied IPv4 request to be rejected: ${testIpArr[0].address}` + ); + + // Denied IPv4-mapped IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get(`https://[::ffff:${testIpArr[0].address}]`); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected denied IPv4-mapped IPv6 request to be rejected: ::ffff:${testIpArr[0].address}` + ); + + // Allowed different IPv4 - allowed by policy + await assert.doesNotReject(async () => { + await instance.get("https://github.com"); + }); + + // Allowed IPv6 - allowed by policy but might fail on some systems due to IPv6 handling + try { + await instance.get("https://ipv6.google.com"); + } catch (err) { + assert.notEqual((err as Error).message, BAD_IP_MESSAGE); + } + }); + + it("deny IPv4-mapped IPv6 address", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + const testIpArr = await promises.lookup(TEST_DOMAIN, { family: 4, all: true }); + policy.addDeniedAddresses(testIpArr.map((ip) => ip.address)); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Denied IPv4 - not allowed by policy + await assert.rejects( + async () => { + await instance.get(`http://${testIpArr[0].address}`); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected denied IPv4 request to be rejected over HTTP: ${testIpArr[0].address}` + ); + + // Denied IPv4-mapped IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get(`http://[::ffff:${testIpArr[0].address}]`); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected denied IPv4-mapped IPv6 request to be rejected over HTTP: ::ffff:${testIpArr[0].address}` + ); + + // Allowed different IPv4 - allowed by policy + await assert.doesNotReject(async () => { + await instance.get("https://github.com"); + }); + + // Allowed IPv6 - allowed by policy but might fail on some systems due to IPv6 handling + try { + await instance.get("https://ipv6.google.com"); + } catch (err) { + assert.notEqual((err as Error).message, BAD_IP_MESSAGE); + } + }); + + it("deny IPv6 address", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + const testIPv6 = "2001:4860:4860::8888"; + policy.addDeniedAddresses([testIPv6]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + // Denied IPv6 - not allowed by policy + await assert.rejects( + async () => { + await instance.get(`http://[${testIPv6}]`); + }, + (err: Error) => { + return err.message === BAD_IP_MESSAGE; + }, + `Expected denied IPv6 request to be rejected: ${testIPv6}` + ); + + // Allowed IPv4 - allowed by policy + await assert.doesNotReject(async () => { + await instance.get("https://github.com"); + }); + }); + + it("both allow and deny", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + const testIps = await promises.lookup(TEST_DOMAIN, { all: true }); + policy.addDeniedAddresses(testIps.map((ip) => ip.address)); + policy.addAllowedAddresses(testIps.map((ip) => ip.address)); + const instance = axios.create({ + httpAgent: policy.getHttpAgent({ keepAlive: false }), + httpsAgent: policy.getHttpsAgent({ keepAlive: false }), + validateStatus: () => true + }); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}`); + }); + }); + + describe("direct IPs - wireserver", () => { + // IPv4: 168.63.129.16 = 0xA83F8110, IPv6: N/A + const wireServerIPs = [ + "168.63.129.16", + + // IPv4-mapped-IPv6 + "::FFFF:168.63.129.16", + "[0:0:0:0:0:FFFF:A83F:8110]" + ]; + + const badWireserverIPs = [ + // dddd + "0xA83f8110", // hex + "2822734096", // dec + "025017700420", // oct + + // d.ddd + "0xA8.0x3F8110", // hex + "168.4161808", // dec + "0250.017700420", // oct + "168.017700420", // mixed + "0250.4161808", + + // d.d.dd + "0xA8.0x3F.0x8110", // hex + "168.63.33040", // dec + "0250.077.0100420", // oct + "168.077.33040", // mixed + "0250.0x3f.0100420", + + // d.d.d.d + "0xA8.0x3F.0x81.0x10", // hex + "0250.077.0201.020", // oct + "168.077.0x81.16", // mixed + "0250.0x3f.0201.020" + ]; + + it("defaults", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + + for (const ip of wireServerIPs) { + assert.equal(policy._isNetworkConnectionAllowed([ip]), false); + } + + for (const ip of badWireserverIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + + it("explicit deny", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addDeniedAddresses(["168.63.129.16"]); + + for (const ip of wireServerIPs) { + assert.equal(policy._isNetworkConnectionAllowed([ip]), false); + } + + for (const ip of badWireserverIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + + it("explicit allow", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + policy.addAllowedAddresses(["168.63.129.16"]); + + for (const ip of wireServerIPs) { + assert.equal(policy._isNetworkConnectionAllowed([ip]), true); + } + + for (const ip of badWireserverIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + }); + + describe("direct IPs - IMDS", () => { + // IPv4: 169.254.169.254 = 0xA9FEA9FE, IPv6: N/A + + const imdsIPs = [ + "169.254.169.254", + + // IPv4-mapped-IPv6 + "::FFFF:169.254.169.254", + "[0:0:0:0:0:FFFF:A9FE:A9FE]" + ]; + + const badImdsIPs = [ + // dddd + "0xA9FEA9FE", // hex + "2852039166", // dec + "025177524776", // oct + + // d.ddd + "0xA9.0xFEA9FE", // hex + "169.16689662", // dec + "0251.077524776", // oct + "169.077524776", // mixed + "0251.16689662", + + // d.d.dd + "0xA9.0xFE.0xA9FE", // hex + "169.254.43518", // dec + "0251.0376.0124776", // oct + "169.0376.43518", // mixed + "0251.0xfe.0124776", + + // d.d.d.d + "0xA9.0xFE.0xA9.0xFE", // hex + "0251.0376.0251.0376", // oct + "169.0376.0xA9.254", // mixed + "0251.0xfe.0251.0376" + ]; + + it("defaults", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + + for (const ip of imdsIPs) { + assert.equal(policy._isNetworkConnectionAllowed([ip]), false); + } + + for (const ip of badImdsIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + + it("explicit deny", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addDeniedAddresses(["169.254.169.254"]); + + for (const ip of imdsIPs) { + assert.equal(policy._isNetworkConnectionAllowed([ip]), false); + } + + for (const ip of badImdsIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + + it("explicit allow", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + policy.addAllowedAddresses(["169.254.169.254"]); + + for (const ip of imdsIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + true, + `Expected IP ${ip} to be allowed by policy` + ); + } + + for (const ip of badImdsIPs) { + assert.equal( + policy._isNetworkConnectionAllowed([ip]), + false, + `Expected IP ${ip} to be denied by policy as invalid IP` + ); + } + }); + }); +}); diff --git a/nodejs/tests/UnitTests/AntiSSRFPolicy.Header.test.ts b/nodejs/tests/UnitTests/AntiSSRFPolicy.Header.test.ts new file mode 100644 index 0000000..5a1294c --- /dev/null +++ b/nodejs/tests/UnitTests/AntiSSRFPolicy.Header.test.ts @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import axios from "axios"; + +import { AntiSSRFError, AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("AntiSSRFPolicy Header Tests", () => { + const BAD_HEADER_MESSAGE = "Request headers or protocol disallowed by policy"; + const TEST_DOMAIN = "ambitious-flower-0611c910f.2.azurestaticapps.net"; + + it("bad inputs", () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1); + + // Invalid arrays + assert.throws( + () => policy.addDeniedHeaders(null as any), + (err: AntiSSRFError) => err.message === "Null argument", + "Expected addDeniedHeaders(null) to throw 'Null argument'" + ); + assert.throws( + () => policy.addRequiredHeaders(null as any), + (err: AntiSSRFError) => err.message === "Null argument", + "Expected addRequiredHeaders(null) to throw 'Null argument'" + ); + + // Invalid array elements + assert.throws( + () => policy.addDeniedHeaders([""]), + (err: AntiSSRFError) => err.message === "Headers cannot be an empty string", + "Expected addDeniedHeaders(['']) to throw empty-header validation error" + ); + assert.throws( + () => policy.addRequiredHeaders([""]), + (err: AntiSSRFError) => err.message === "Headers cannot be an empty string", + "Expected addRequiredHeaders(['']) to throw empty-header validation error" + ); + assert.throws( + () => policy.addDeniedHeaders(["X-Valid-Header", null as any, "Another-Header"]), + (err: AntiSSRFError) => err.message === "Headers cannot be null or undefined", + "Expected addDeniedHeaders(['X-Valid-Header', null, 'Another-Header']) to throw null-header validation error" + ); + assert.throws( + () => policy.addRequiredHeaders([null as any, "X-Test-Header"]), + (err: AntiSSRFError) => err.message === "Headers cannot be null or undefined", + "Expected addRequiredHeaders([null, 'X-Test-Header']) to throw null-header validation error" + ); + }); + + it("check defaults", () => { + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.InternalOnly).requiredHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.InternalOnly).deniedHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1).requiredHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1).deniedHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1).deniedHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest).requiredHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest).deniedHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.None).requiredHeaders.length, 0); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.None).deniedHeaders.length, 0); + }); + + it("required header", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["X-Test-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.rejects( + async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "Not-X-Test-Header": "test-value" + } + }); + }, + (err: Error) => err.message === BAD_HEADER_MESSAGE, + "Expected request missing required header to be rejected" + ); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "X-Test-Header": "test-value" + } + }); + }); + }); + + it("denied header", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addDeniedHeaders(["X-Test-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.rejects( + async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "X-Test-Header": "test-value" + } + }); + }, + (err: Error) => err.message === BAD_HEADER_MESSAGE, + "Expected request containing denied header to be rejected" + ); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "Not-X-Test-Header": "test-value" + } + }); + }); + }); + + it("both required and denied", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["X-Test-Header"]); + policy.addDeniedHeaders(["X-Test-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.rejects( + async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "X-Test-Header": "test-value" + } + }); + }, + (err: Error) => err.message === BAD_HEADER_MESSAGE, + "Expected request to be rejected when same header is both required and denied" + ); + + await assert.rejects( + async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "Not-X-Test-Header": "test-value" + } + }); + }, + (err: Error) => err.message === BAD_HEADER_MESSAGE, + "Expected request to be rejected when required header is missing and denied header config also exists" + ); + }); + + it("with XFF header", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addXFFHeader = true; + policy.addRequiredHeaders(["X-Forwarded-For", "X-Test-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Forwarded-For`, { + headers: { + "X-Test-Header": "test-value" + } + }); + }); + + const policy2 = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy2.addXFFHeader = true; + policy2.addDeniedHeaders(["X-Forwarded-For", "Not-X-Test-Header"]); + const instance2 = axios.create({ + httpAgent: policy2.getHttpAgent(), + httpsAgent: policy2.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.rejects( + async () => { + await instance2.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Forwarded-For`, { + headers: { + "X-Test-Header": "test-value" + } + }); + }, + (err: Error) => err.message === BAD_HEADER_MESSAGE, + "Expected request to be rejected when X-Forwarded-For is denied and addXFFHeader is enabled" + ); + }); + + it("holds on redirect", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["X-Required-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}/api/redirect?num=3`, { + headers: { + "X-Required-Header": "test-value" + } + }); + }); + }); + + it("case insensitive headers", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.addRequiredHeaders(["X-Test-Header"]); + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}/api/header-check?header=X-Test-Header`, { + headers: { + "x-test-header": "test-value" + } + }); + }); + }); +}); diff --git a/nodejs/tests/UnitTests/AntiSSRFPolicy.Scheme.test.ts b/nodejs/tests/UnitTests/AntiSSRFPolicy.Scheme.test.ts new file mode 100644 index 0000000..9108e15 --- /dev/null +++ b/nodejs/tests/UnitTests/AntiSSRFPolicy.Scheme.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; +import axios from "axios"; + +import { AntiSSRFPolicy, PolicyConfigOptions } from "../../src"; + +describe("AntiSSRFPolicy Scheme Tests", () => { + const BAD_SCHEME_MESSAGE = "Request headers or protocol disallowed by policy"; + const TEST_DOMAIN = "ambitious-flower-0611c910f.2.azurestaticapps.net"; + + it("check defaults", () => { + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.InternalOnly).allowPlainTextHttp, false); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyV1).allowPlainTextHttp, false); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.ExternalOnlyLatest).allowPlainTextHttp, false); + assert.strictEqual(new AntiSSRFPolicy(PolicyConfigOptions.None).allowPlainTextHttp, false); + }); + + it("on true", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.doesNotReject(async () => { + await instance.get(`http://${TEST_DOMAIN}`); + }); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}`); + }); + }); + + it("on false", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = false; + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + await assert.rejects( + async () => { + await instance.get(`http://${TEST_DOMAIN}`); + }, + (err: Error) => err.message === BAD_SCHEME_MESSAGE, + "Expected HTTP request to be rejected when allowPlainTextHttp is false" + ); + + await assert.doesNotReject(async () => { + await instance.get(`https://${TEST_DOMAIN}`); + }); + }); + + it("rejects non-http schemes", async () => { + const policy = new AntiSSRFPolicy(PolicyConfigOptions.None); + policy.allowPlainTextHttp = true; + const instance = axios.create({ + httpAgent: policy.getHttpAgent(), + httpsAgent: policy.getHttpsAgent(), + validateStatus: () => true + }); + + const nonHttpSchemes = [ + "ws://example.com", + "wss://example.com", + "ftp://example.com", + "gopher://example.com", + "file:///etc/passwd", + "ldap://example.com", + "ldaps://example.com", + "mailto:test@example.com", + "tel:+1234567890", + // "data:text/plain;base64,SGVsbG8=", Axios handles data: separately + "javascript:alert('xss')", + "custom://example.com" + ]; + + for (const url of nonHttpSchemes) { + await assert.rejects(async () => { + await instance.get(url); + }, `Expected request with non-HTTP scheme to be rejected: ${url}`); + } + }); +}); diff --git a/nodejs/tests/UnitTests/CIDRBlock.test.ts b/nodejs/tests/UnitTests/CIDRBlock.test.ts new file mode 100644 index 0000000..07de7e0 --- /dev/null +++ b/nodejs/tests/UnitTests/CIDRBlock.test.ts @@ -0,0 +1,326 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "assert"; + +import { CIDRBlock } from "../../src/Helpers/CIDRBlock"; +import { AntiSSRFError } from "../../src"; +import { BlockList } from "net"; + +const toParsedAddress = (ip: string): string => CIDRBlock._parseIPAddress(ip)[0]; + +describe("CIDRBlock Tests", () => { + it("bad inputs", () => { + // Parse - null input + assert.throws( + () => CIDRBlock._parseIPAddress(null!), + (err: AntiSSRFError) => err.message === "Null argument" + ); + + // Parse - too many / + assert.throws( + () => CIDRBlock._parseIPAddress("192.168.1.0/24"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + + // Parse - invalid IP address + assert.throws( + () => CIDRBlock._parseIPAddress("256.256.256.256"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("192.168.1.300"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("not-an-ip"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("999.999.999.999"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("[127.0.0.1]"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.0.0.01"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress(""), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress(" 127.0.0.1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.0.0.1 "), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.0.0"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.0.0.1.5"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.-1.0.1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("127.0.0.+1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("01.2.3.4"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv4 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("gggg::1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("2001:::1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("2001::db8::1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("2001:db8:1:2:3:4:5:6:7"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("2001:db8::g1"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + assert.throws( + () => CIDRBlock._parseIPAddress("::ffff:192.168.1.999"), + (err: AntiSSRFError) => err.message.includes("Invalid IPv6 address") + ); + + // Parse - null input + assert.throws( + () => CIDRBlock._parseCIDR(null!), + (err: AntiSSRFError) => err.message === "Null argument" + ); + + // Parse - too many / + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/24/16"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("10.0.0.0/8/"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("2001:db8::/32/64"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + + // Parse - invalid IP address + assert.throws( + () => CIDRBlock._parseCIDR("256.256.256.256/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.300/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("not-an-ip/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("999.999.999.999"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("gggg::1/64"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + + // Parse - invalid prefix format + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/abc"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/24.5"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/+24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("127.0.0.0/024"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + + // Parse - invalid prefix length + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/33"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/-1"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.1.0/255"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("2001:db8::/129"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("2001:db8::/-5"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + }); + + it("contains ipv4 returns expected result", () => { + // Standard decimal format + const block1 = CIDRBlock._parseCIDR("192.168.1.0/24"); + const denyList1 = new BlockList(); + denyList1.addSubnet(block1.getAddress(), block1.getPrefix(), "ipv6"); + assert.equal(denyList1.check(toParsedAddress("192.168.1.1"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("::ffff:192.168.1.255"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("192.168.2.1"), "ipv6"), false); + + // Test without prefix length (defaults to /32) + const block2 = CIDRBlock._parseCIDR("127.0.0.1"); + const denyList2 = new BlockList(); + denyList2.addSubnet(block2.getAddress(), block2.getPrefix(), "ipv6"); + assert.equal(denyList2.check(toParsedAddress("127.0.0.1"), "ipv6"), true); + assert.equal(denyList2.check(toParsedAddress("::ffff:127.0.0.2"), "ipv6"), false); + + // Node.js parsing is strict dotted-quad for IPv4, so other C# formats are invalid. + assert.throws( + () => CIDRBlock._parseCIDR("0300.0250.001.000/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("0xC0.0xA8.0x1.0x0/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.0250.1.0x0/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.256/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.11010304/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("3232235776/24"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("0xC0A80101"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + assert.throws( + () => CIDRBlock._parseCIDR("192.168.257"), + (err: AntiSSRFError) => err.message.includes("Invalid CIDR block") + ); + }); + + it("contains ipv6 returns expected result", () => { + // Standard full format + const block1 = CIDRBlock._parseCIDR("2001:0db8:0000:0000:0000:0000:0000:0000/32"); + const denyList1 = new BlockList(); + denyList1.addSubnet(block1.getAddress(), block1.getPrefix(), "ipv6"); + assert.equal(denyList1.check(toParsedAddress("2001:db8::1"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("2001:db8:ffff::1"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("2001:db9::1"), "ipv6"), false); + + // Leading compression + const block3 = CIDRBlock._parseCIDR("::1/128"); + const denyList3 = new BlockList(); + denyList3.addSubnet(block3.getAddress(), block3.getPrefix(), "ipv6"); + assert.equal(denyList3.check(toParsedAddress("::1"), "ipv6"), true); + assert.equal(denyList3.check(toParsedAddress("::2"), "ipv6"), false); + + // Trailing compression + const block4 = CIDRBlock._parseCIDR("2001:db8:1::/48"); + const denyList4 = new BlockList(); + denyList4.addSubnet(block4.getAddress(), block4.getPrefix(), "ipv6"); + assert.equal(denyList4.check(toParsedAddress("2001:db8:1::1"), "ipv6"), true); + assert.equal(denyList4.check(toParsedAddress("2001:db8:1:ffff::1"), "ipv6"), true); + assert.equal(denyList4.check(toParsedAddress("2001:db8:2::1"), "ipv6"), false); + + // Middle compression + const block5 = CIDRBlock._parseCIDR("2001:db8::1:0:0:1/64"); + const denyList5 = new BlockList(); + denyList5.addSubnet(block5.getAddress(), block5.getPrefix(), "ipv6"); + assert.equal(denyList5.check(toParsedAddress("2001:db8::1"), "ipv6"), true); + assert.equal(denyList5.check(toParsedAddress("2001:db8:0:0:ffff::"), "ipv6"), true); + assert.equal(denyList5.check(toParsedAddress("2001:db9::1"), "ipv6"), false); + + // :: format + const block6 = CIDRBlock._parseCIDR("::ffff:192.168.1.0/120"); + const denyList6 = new BlockList(); + denyList6.addSubnet(block6.getAddress(), block6.getPrefix(), "ipv6"); + assert.equal(denyList6.check(toParsedAddress("192.168.1.1"), "ipv6"), true); + assert.equal(denyList6.check(toParsedAddress("::ffff:192.168.1.255"), "ipv6"), true); + assert.equal(denyList6.check(toParsedAddress("::ffff:192.168.2.1"), "ipv6"), false); + + // Mixed case hex digits + const block8 = CIDRBlock._parseCIDR("2001:DB8:aBCD:Ef01::/64"); + const denyList8 = new BlockList(); + denyList8.addSubnet(block8.getAddress(), block8.getPrefix(), "ipv6"); + assert.equal(denyList8.check(toParsedAddress("2001:db8:abcd:ef01::1"), "ipv6"), true); + assert.equal(denyList8.check(toParsedAddress("2001:db8:abcd:ef02::1"), "ipv6"), false); + + // Test without prefix length (defaults to /128) + const block9 = CIDRBlock._parseCIDR("2001:db8::1"); + const denyList9 = new BlockList(); + denyList9.addSubnet(block9.getAddress(), block9.getPrefix(), "ipv6"); + assert.equal(denyList9.check(toParsedAddress("2001:db8::1"), "ipv6"), true); + assert.equal(denyList9.check(toParsedAddress("2001:db8::2"), "ipv6"), false); + + const block10 = CIDRBlock._parseCIDR("::1"); + const denyList10 = new BlockList(); + denyList10.addSubnet(block10.getAddress(), block10.getPrefix(), "ipv6"); + assert.equal(denyList10.check(toParsedAddress("::1"), "ipv6"), true); + assert.equal(denyList10.check(toParsedAddress("::2"), "ipv6"), false); + + const block11 = CIDRBlock._parseCIDR("::ffff:192.168.1.1"); + const denyList11 = new BlockList(); + denyList11.addSubnet(block11.getAddress(), block11.getPrefix(), "ipv6"); + assert.equal(denyList11.check(toParsedAddress("192.168.1.1"), "ipv6"), true); + assert.equal(denyList11.check(toParsedAddress("::ffff:192.168.1.2"), "ipv6"), false); + }); + + it("contains ipv6 with scope returns expected result", () => { + // Scoped addresses should have their scope stripped and still match if the address is in the block. + const block1 = CIDRBlock._parseCIDR("fe80::/10"); + const denyList1 = new BlockList(); + denyList1.addSubnet(block1.getAddress(), block1.getPrefix(), "ipv6"); + assert.equal(denyList1.check(toParsedAddress("fe80::1%eth0"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("fe80::1%1"), "ipv6"), true); + assert.equal(denyList1.check(toParsedAddress("2001:db8::1%eth0"), "ipv6"), false); + }); +}); diff --git a/nodejs/tsconfig.json b/nodejs/tsconfig.json new file mode 100644 index 0000000..ed79c1c --- /dev/null +++ b/nodejs/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ESNext", + "outDir": "out", + "esModuleInterop": true, + "declaration": true, + "removeComments": true, + "types": ["node", "mocha"] + }, + "include": ["src", "tests"], + "exclude": ["out"] +} diff --git a/scripts/build-ip-ranges-nodejs.sh b/scripts/build-ip-ranges-nodejs.sh index b0b27d6..a58d2c0 100755 --- a/scripts/build-ip-ranges-nodejs.sh +++ b/scripts/build-ip-ranges-nodejs.sh @@ -11,11 +11,13 @@ set -e # Get the directory where this script is located SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" JSON_FILE="$SCRIPT_DIR/../config/IPAddressRanges.json" -TS_FILE="$SCRIPT_DIR/../nodejs/config/IPAddressRanges.ts" +TS_FILE="$SCRIPT_DIR/../nodejs/src/IPAddressRanges.ts" # Check if jq is available if ! command -v jq &> /dev/null; then - echo "Error: jq is required but not installed. Install with: brew install jq" + echo "Error: jq is required but not installed." + echo "Install on WSL/Ubuntu: sudo apt-get update && sudo apt-get install -y jq" + echo "Install on macOS: brew install jq" exit 1 fi